webpack学习入门
内容提纲
- electron-vue项目中的webpack工程实例
- 思考
- webpack与gulp/grunt
- HMR
electron-vue项目中的webpack工程实例
从electron-vue项目中的实际使用例子来入手
如下是 webpack.renderer.config.js
文件
'use strict'
process.env.BABEL_ENV = 'renderer'
const path = require('path')
const { dependencies } = require('../package.json')
const webpack = require('webpack')
const BabiliWebpackPlugin = require('babili-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
let whiteListedModules = ['vue']
let rendererConfig = {
/**
* 控制打包后代码的形式,影响调试。当前设置为cheap-module-eval-source-map,调试时仅可看到原始源代码
*(仅限行),如果同时使用了代码压缩插件,代码被合成一行,将无法获取有效的调试内容。
*/
devtool: '#cheap-module-eval-source-map',
/**
* webpack处理项目的入口文件,以该文件作为构建其内部依赖图的开始。
* 通常是项目的 index 或 main 文件
* 注意 entry 可以是一个数组,即 webpack 可以处理多个入口文件,将其
*/
entry: {
renderer: path.join(__dirname, '../src/renderer/main.js')
},
/**
* 声明外部依赖,在此声明的文件,即使在工程中使用import引入,也不会被打到bundle中。
* 一般我们会把package.json中的dependencies排除在外。
* 当我们需要使用cdn引用第三方库时,我们也要将其放在external中,比如使用jquery cdn
* 思考1:node_modules中的三方库被排除在打包的bundle文件外,为什么我们的程序在打包后仍然能够使用
* 三方库
*/
externals: [
...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
],
/**
* 模块是 webpack 的核心,webpack 帮我们把文件打包成模块,然后我们就可以使用 import 的方式来
* 使用。webpack 通过 loader 来各种处理文件,因此我们可以在项目中使用模块化的方法引用任何类型的文件
*/
module: {
rules: [
{
/**
* test 做正则匹配,所有符合该匹配规则的文件都将应用该 loader 规则
*/
test: /\.(js|vue)$/,
/**
* enfore 用来定义规则的执行顺序,取值 ['pre', 'post'],此处为 pre,将在所有 loader 执行前
* 执行。即在所有 loader 规则之前,使用 eslint-loader 对代码做静态检查
*/
enforce: 'pre',
/**
* exclude 排除不想应用该规则的目录
*/
exclude: /node_modules/,
/**
* useEntry 配置loader
*/
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}
},
{
test: /\.css$/,
/**
* ExtractTextPlugin 插件,用于将 *.css 分离到单独的 style.css 文件中,而不是作为样式
* 放入 bundle.js 文件中
* fallback 是当 CSS 没有被提取时应用的 loader
* style-loader 的作用是使用 <style></style> 标签将样式添加到页面 dom 中
*/
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader'
})
},
{
test: /\.scss$/,
/**
* 不同于 use 的形式,loader 写成内联的方式
* 执行顺序是“从右到左,从下到上“,即 sass-loader -> css-loader -> vue-style-loader
* loader 之间通过 ! 分隔,如果需要为 loader 配置参数,则可以使用 ?key=value&foo=bar 的形
* 式,类似于 url 的参数。
*/
loader: 'vue-style-loader!css-loader!sass-loader'
},
{
test: /\.html$/,
use: 'vue-html-loader'
},
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.json$/,
use: 'json-loader'
},
{
test: /\.node$/,
use: 'node-loader'
},
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: process.env.NODE_ENV === 'production',
loaders: {
/**
* 带参数的 loader
*/
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
scss: 'vue-style-loader!css-loader!sass-loader'
}
}
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
/**
* query 老的写法,等同于 options
*/
query: {
limit: 10000,
name: 'imgs/[name].[ext]'
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'fonts/[name].[ext]'
}
}
},
{
test: /\.(ogg)$/,
use: 'file-loader'
}
]
},
plugins: [
new ExtractTextPlugin('styles.css'),
/**
* HtmlWebpackPlugin 这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文
* 件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)
* 当我们发布一个网站应用时,我们需要通过生成带 hash 值的文件名来更新浏览器对静态文件的缓存
* 这样会有一个问题,我们每次使用 webpack 打包后都需要手动修改 index.html 文件中引用的
* bundle.[hash].js 文件名,显然很麻烦。
* HtmlWebpackPlugin 的引入就是为了解决此问题,通过配置一个 index 模板,webpack 会在每次打包完成
* 后,将 bundle.[hash].js 的引用注入 index.html 中
*/
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true
},
nodeModules: process.env.NODE_ENV !== 'production'
? path.resolve(__dirname, '../node_modules')
: false
}),
/**
* HotModuleReplacementPlugin webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需
* 进行页面刷新。使用 HMR 的方法:
* 1.在webpack配置文件中添加HMR插件;
* 2.在Webpack Dev Server中添加“hot”参数; 在我们的electron-vue应用中使用了接口调用的方式,查看
* dev-runner.js 文件
*/
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
/**
* 自动加载模块,并定义到全局变量,随时可以引用。相当于 import 了一个全局对象
* new webpack.ProvidePlugin({
* identifier: 'module1',
* identifier: ['module1', 'property1'],
* // ...
* });
* 任何时候,当 identifier 被当作未赋值的变量时,module 就会自动被加载,并且 identifier 会被这个
* module 输出的内容所赋值。
* 支持支持命名导出,如我们想全局使用 _.map 这个方法,那么我们可以这样来引入:
* new webpack.ProvidePlugin({
* _map: ['lodash', 'map']
* });
* 在项目中即可以使用 _map 来使用 _.map 方法
*/
new webpack.ProvidePlugin({
jQuery: "jquery",
jquery: "jquery",
$: "jquery",
"window.jQuery": "jquery"
})
],
/**
* 定义打包文件名,[name] 参数由 entry 中的key决定,此处为render.js
* 这里文件名没有使用 hash 值,是因为在 electron 项目中,升级不需要考虑浏览器缓存的问题,整个应用都会
* 被替换
*/
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron')
},
/**
* 在此可以定义 import 或 require 的别名,来确保模块引入变得更简单。
* 当目录下的文件没有文件后缀时,使用 extensions 中的后缀依次尝试解析
*/
resolve: {
alias: {
'@': path.join(__dirname, '../src/renderer'),
'vue$': 'vue/dist/vue.esm.js',
'config' : path.join(__dirname, '../config'),
'components': path.join(__dirname, '../src/renderer/components'),
'utils': path.join(__dirname, '../src/renderer/utils'),
'renderer': path.join(__dirname, '../src/renderer'),
'services': path.join(__dirname, '../src/renderer/services'),
'store': path.join(__dirname, '../src/renderer/store'),
'router': path.join(__dirname, '../src/renderer/router'),
'plugins': path.join(__dirname, '../src/renderer/plugins'),
'css': path.join(__dirname, '../src/renderer/css'),
'images': path.join(__dirname, '../src/renderer/images'),
'jquery': path.join(__dirname, '../node_modules/jquery/src/jquery')
},
extensions: ['.js', '.vue', '.json', '.css', '.node']
},
/**
* 构建目标, https://webpack.docschina.org/configuration/target
*/
target: 'electron-renderer'
}
/**
* Adjust rendererConfig for development settings
*/
if (process.env.NODE_ENV !== 'production') {
/**
* DefinePlugin 允许创建一个在编译时可以配置的全局常量。
* 比如定义后端接口的url
* https://webpack.docschina.org/plugins/define-plugin/
*/
rendererConfig.plugins.push(
new webpack.DefinePlugin({
'__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
})
)
}
/**
* Adjust rendererConfig for production settings
*/
if (process.env.NODE_ENV === 'production') {
rendererConfig.devtool = ''
rendererConfig.plugins.push(
/**
* 这是一款基于 Babel 的压缩工具,支持 es6 的一些特性,取代 UglifyJS
*/
new BabiliWebpackPlugin({
removeConsole: true,
removeDebugger: true
}),
/**
* Copies individual files or entire directories to the build directory.
* 一般会使用插件把需要使用的静态文件考到构建后对应的目录
*/
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/electron/static'),
ignore: ['.*']
}
]),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
/**
* 用于 webpack2 对 webpack1 配置的兼容,抹平差异
*/
new webpack.LoaderOptionsPlugin({
minimize: true
})
)
}
module.exports = rendererConfig
思考1:external
node_modules中的三方库被排除在打包的bundle文件外,为什么我们的程序在打包后仍然能够使用三方库
// package.json
{
...
"build": {
...
"files": [
"dist/electron",
"node_modules/",
"package.json"
]
}
}
在 package.json 文件中,配置了 files 文件,electron-build 在打包时会将 files 中的文件夹全部打包到工程中,即 resources/app.asar
文件
我们使用命令查看
asar list ./resources/app.asar
可以看到如下一系列文件
/node_modules/argparse/lib/action/store/false.js
/node_modules/argparse/lib/action/store/true.js
/node_modules/argparse/lib/action/append
/node_modules/argparse/lib/action/append/constant.js
...
/dist
/dist/electron
/dist/electron/index.html
/dist/electron/main.js
/dist/electron/renderer.js
/dist/electron/static/update/process.html
/dist/electron/static/update/process.js
/dist/electron/static/update/libs
...
/dist/electron/fonts
/dist/electron/fonts/element-icons.ttf
webpack与gulp/grunt
Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具
Grunt和Gulp的工作方式
在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。
Webpack的工作方式是
把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。
如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。
开发模式热更新
开发模式热更新分为两部分:
- 监控代码修改,并自动重新编译打包
- 通知浏览器同步修改的内容(注意,不是刷新浏览器重新加载,是动态替换部分代码,保持原页面的数据状态,这是HMR的关键特性)
通常使用webpack-dev-server
- 该工具包括自动监控代码编译,以及HMR。
-
webpack-dev-server
将代码打包在内存中,所以修改代码时在项目下找不到动态生成的编译文件。
注意
依靠HMR的热更新机制,我们可以享受修改的文件被实时同步到浏览器的便利,但对于页面来说,这并不是全部。我们仍然需要在页面的js代码中做一些处理。
比如:我们在按钮上绑定了一个click事件的处理函数handleClick
,我们修改了handleClick
的代码,它被推送到浏览器中,但是页面并不会自动替换之前绑定的click事件,当我们点击按钮时,仍然得到旧的响应。
所以除了使用webpack-dev-server
,我们需要在js代码中监听HMR 的 accept 方法,在此方法中更新类似的处理函数。
这也就是为什么在react项目中,我们需要使用react-hot-loader
库,本质上是实现了HMR 的 accept 的处理。