使用 vue-loader 的時候,在 webpack 中如何配置?javascript
vue-loader 如何將一個 .vue 文件 轉化爲 瀏覽器可識別的.js?css
css scoped & 深度做用選擇器html
css modulevue
.vue文件 是怎麼實現熱更新的?java
tree shaking 的反作用node
使用 vue-loader 的以前, 咱們須要安裝一些必要的 loader。。webpack
必需的 loader 包括:vue-loader、vue-style-loader、vue-template-compiler、css-loader。 可能須要的 loader 包含:sass-loader、less-loader、url-loader 等。web
一個包含 vue-loader 的簡單 webpack配置 以下:json
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { VueLoaderPlugin } = require('vue-loader')
const isProduction = process.env.NODE_ENV === 'production'
const extractLoader = {
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
hmr: process.env.NODE_ENV === 'development'
},
}
const cssExtractplugin = new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
ignoreOrder: false
})
const webpackConfig = {
entry: {...},
output: {...},
optimization: {...},
resolve: {...},
modules: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.css$/,
oneOf: [{
resourceQuery: /\?vue/,
use: [isProduction ? extractLoader : 'vue-style-loader', 'css-loader']
}, {
use: [isProduction ? extractLoader : 'style-loader', 'css-loader']
}]
},
...
]
},
plugins: [
new VueLoaderPlugin(),
isProduction ? cssExtractplugin : ''
]
}
複製代碼
注意,當使用的 vue-loader 版本爲 15.x.x 時, 必須使用 vue-loader 提供的 VueLoaderPlugin。api
經過 vue-loader, webpack 能夠將 .vue 文件 轉化爲 瀏覽器可識別的javascript。
vue-loader 的工做流程, 簡單來講,分爲如下幾個步驟:
將一個 .vue 文件 切割成 template、script、styles 三個部分。
template 部分 經過 compile 生成 render、 staticRenderFns。
獲取 script 部分 返回的配置項對象 scriptExports。
styles 部分,會經過 css-loader、vue-style-loader, 添加到 head 中, 或者經過 css-loader、MiniCssExtractPlugin 提取到一個 公共的css文件 中。
使用 vue-loader 提供的 normalizeComponent 方法, 合併 scriptExports、render、staticRenderFns, 返回 構建vue組件須要的配置項對象 - options, 即 {data, props, methods, render, staticRenderFns...}。
經過 vue-loader 生成的 js 文件 以下:
// 從 template區域塊 獲取 render、 staticRenderFns 方法
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
// 從 script區域塊 獲取 組件的配置項對象
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
// 獲取 styles區域塊的內容
import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
// 獲取 styles(scoped)區域塊的內容
import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"
/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// 返回構建組件須要的配置項對象, 包含 data、props、render、staticRenderFns 等
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
"7ba5bd90",
null
)
component.options.__file = "src/App.vue"
// 輸出組件完整的配置項
export default component.exports
複製代碼
當 .vue 文件 中的 style 標籤 有 scoped 屬性時,它的 css 樣式 只做用於當前 組件 中的 元素。
css scoped 的 工做流程 以下:
使用 vue-loader 處理 .vue 文件, 根據 .vue 文件 的 請求路徑 和 文件內容, 生成 .vue 文件 的 hash 值, 如:7ba5bd90;
若是 .vue 文件 的 某一個 style 標籤 有 scoped 屬性, 爲 .vue 文件 生成一個 scopedId,scopedId 的格式爲 data-v-hash, 如:data-v-7ba5bd90;
使用 vue-loader 從 .vue 文件 中獲取 style區域塊(scoped) 的 樣式內容(字符串);若是使用了 less 或者 sass, 要使用 less-loader 或者 sass-loader 處理 樣式內容,使 樣式內容 變爲 瀏覽器可識別的css樣式; 而後使用 PostCSS 提供的 parser 處理 樣式內容, 爲 樣式內容 中的每個 css選擇器 添加 [data-v-hash]; 再使用 css-loader;最後使用 style-loader 把 css 樣式 添加到 head 中或者經過 miniCssExtractPlugin 將 css 樣式 提取一個公共的 css 文件中。
經過 normalizer 方法返回 完整的組件配置項 options, options 中有屬性 _scopeId, 如 _scopedId: data-v-7ba5bd90;
使用 組件配置項 options 構建組件實例, 給 組件 中每個 dom元素 添加屬性: data-v-hash。
經歷上述過程,style(scoped) 中的樣式就變成了 組件的私有樣式。
咱們能夠經過 >>> 操做符, 在 組件 中修改 子組件 的 私有樣式。
// child component
.hello {...}
// parent component
<style scoped>
.parant .hello {...}
.parent >>> .hello {...}
</style>
// 進過 postCSS 處理之後的 css
.parent .hello[data-v-xxx] {...} // 沒法影響子組件
.parant[data-v-xxx] .hello {....} // 可影響子組件
複製代碼
有些像 Sass 之類的 預處理器 沒法 正確解析 >>>。這種狀況下咱們可使用 /deep/ 或 ::v-deep 操做符取而代之,二者都是 >>> 的 別名,一樣能夠正常工做。
深度做用選擇器, 必須在含有 scoped 屬性 的 style 標籤中使用,不然無效。 這是由於 >>>、/deep/、::v-deep 須要被 postCSS 解析才能起做用。 只有 style 標籤 中有 scoped 屬性, 樣式內容 纔會被 postCSS 解析。
postCSS 解析樣式內容的時候, 會給 >>> 操做符 前面 的 css選擇器 添加 [data-v-hash]。
注意: 父組件 中修改 子組件 的 私有樣式 時, 父組件 中的 樣式的優先級 要大於 子組件 中的 樣式的優先級, 不然會致使 父組件中定義的樣式不生效。
咱們也能夠在 .vue 文件 的 style 標籤 上添加 module 屬性, 使得 style 標籤 中的 樣式 變爲 組件私有,具體使用方法詳見 - 官網。
css modules 和 css scoped 均可以使 樣式 變爲 組件私有,可是 原理 不同。
css scoped 的實質是利用 css屬性選擇器 使得 樣式 稱爲 局部樣式,而 css modules 的實質是讓 樣式的類名、id名惟一 使得 樣式 稱爲 局部樣式。
css modules 的 工做流程 以下:
使用 vue-loader 處理 .vue 文件, 將 .vue 文件內容 轉化爲 js 代碼。 若是 .vue 文件 中的 style 標籤 中有 module 屬性, 向 js 代碼 中注入一個 injectStyle 方法, 以下:
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=3512ffa2&scoped=true&"
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
import style0 from "./App.vue?vue&type=style&index=0&module=a&lang=css&"
import style1 from "./App.vue?vue&type=style&index=1&id=3512ffa2&module=true&scoped=true&lang=css&"
// 經過injectStyle方法, 會向vue實例中添加屬性
function injectStyles (context) {
// 對應 <style module="a">...</style>
// 給vue實例添加屬性a, 對應的值爲使用css-loader處理樣式內容之後返回的對象
this["a"] = (style0.locals || style0)
// 對應 <style module>...</style>
// 給vue實例添加屬性$style, 對應的值爲使用css-loader處理樣式內容之後返回的對象
this["$style"] = (style1.locals || style1)
}
/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// normalize 會返回一個組件完整配置項對象
// 在執行過程當中, 會將render方法從新包裝成 renderWithStyleInjection 方法
// 執行 renderWithStyleInjection 方法時的時候, 先執行 injectStyles 方法, 再執行 原來的render 方法
var component = normalizer(
script,
render,
staticRenderFns,
false,
injectStyles,
"3512ffa2",
null
)
export default component.exports" 複製代碼
使用 css-loader 處理 .vue 文件 的 style 區域塊,會將 style 區域塊 中的樣式內容, 轉化爲 js 代碼, 以下:
exports = module.exports = require("../node_modules/_css-loader@3.2.0@css-loader/dist/runtime/api.js")(false);
// Module
exports.push([module.id, "\n#_3cl756BP8kssTYTEsON-Ao {\n font-family: 'Avenir', Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n._3IbrnaW__7RJMXk4rh9tW- {\n background-color: blue;\n}\n", ""]);
// Exports
exports.locals = {
// app是id名
"app": "_3cl756BP8kssTYTEsON-Ao",
// class 是 類名
"class1": "_3IbrnaW__7RJMXk4rh9tW-"
}
複製代碼
在處理過程當中, css-loader 會將樣式中的 類名、id名 等用一個 惟一的命名代替。
在執行 步驟1 的代碼時,會執行上面的代碼, 返回一個 對象, 即 步驟一 中的 style0 、style1, 格式以下:
// css樣式內容會經過 style-loader 提供的方法添加到 head 中
// 或者被 miniCssExtractPlugin 提取到一個 公共的css文件 中
style0 = [[css模塊 id, css樣式內容字符串, ''], ...]
style0.locals = {
"app": "_3cl756BP8kssTYTEsON-Ao",
"class1": "_3IbrnaW__7RJMXk4rh9tW-"
}
複製代碼
運行項目執行打包之後的js代碼, 即 步驟1中的代碼, 獲取 render、staticRenderFns、scriptExprots、 style0、style1, 而後經過 normalizer 方法返回 組件完整配置項 - options。 在執行過程當中,將 render 方法從新包裝成 renderWithStyleInjection 方法。
構建 vue 實例 時,執行 renderWithStyleInjection 方法, 此時會 先 執行 injectStyles 方法,給 vue 實例 添加 $style、a 屬性,屬性值爲 stlye0.locals、style1.locals, 再執行原來的 render 方法。
這樣, 咱們就能夠經過 vue 實例 的 $style、a 屬性訪問 樣式 的 類名、id名。
開發模式 下,當使用 vue-loader、 vue-style-loader 處理 .vue 文件 的時候, 會向 生成的js代碼 中注入與 熱更新 相關的代碼邏輯。 當咱們修改 .vue 文件 時, dev-server 會通知 瀏覽器 進行 熱更新。
.vue 文件 的 各個區域塊(template、script、styles) 對應的 熱更新邏輯 都不同。
vue-loader 會在 打包代碼 中注入 熱更新 template、script 區域塊 的代碼,以下:
// 從 template區域塊 獲取 render、 staticRenderFns 方法
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
// 從 script區域塊 獲取 組件的配置項對象
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
// 獲取 styles區域塊的內容
import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
// 獲取 styles(scoped)區域塊的內容
import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"
/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// 返回構建組件須要的配置項對象, 包含 data、props、render、staticRenderFns 等
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
"7ba5bd90",
null
)
/* hot reload */
// .vue 文件的 script 區域塊更改時, 客戶端執行這一段代碼
if (module.hot) {
var api = require("D:\\study\\demo\\webpack\\webpack-4-demo\\node_modules\\_vue-hot-reload-api@2.3.3@vue-hot-reload-api\\dist\\index.js")
api.install(require('vue'))
if (api.compatible) {
module.hot.accept()
if (!api.isRecorded('7ba5bd90')) {
api.createRecord('7ba5bd90', component.options)
} else {
// 執行 reload 方法, 觸發更新
// 使用 新的 options 替換原來的 options
api.reload('7ba5bd90', component.options)
}
module.hot.accept("./App.vue?vue&type=template&id=7ba5bd90&scoped=true&", function () {
// 當 .vue 文件的 template 區域塊更改時, 客戶端執行這一段代碼
// 使用新的 render、staticRenderFns 更新原來的render、staticRenderFns
api.rerender('7ba5bd90', {
render: render,
staticRenderFns: staticRenderFns
})
})
}
}
component.options.__file = "src/App.vue"
// 輸出組件完整的配置項
export default component.exports
複製代碼
若是咱們只修改了 .vue 文件 的 script 部分, 客戶端(即瀏覽器) 會進行 熱更新, 過程以下:
服務端 經過 websocket 鏈接 通知 客戶端 更新;
客戶端 經過 動態添加script元素 的方式獲取 更新之後的打包文件;
安裝打包文件,即執行 新的打包文件 中的 js 代碼, 使用 打包文件中的 module 更新瀏覽器緩存的同名 module;
從新安裝組件對應的 module, 即 從新執行組件對應的js代碼, 獲取 render、staticRenderFns 和 新的 scriptExports, 從新生成 組件 對應的 完整配置項;
執行 api 提供的 reload 方法, 更新組件。
在 reload 方法中,會經過執行 父組件實例 的 $forceUpdate 方法來 更新組件。
更新組件的時候, 因爲組件 的 配置項(data、props、methods等屬性) 發生變化, 須要爲 組件 生成 新的構造函數 VueComponent, 而後使用 新的構造函數,構建 新的組件實例。
即, 每次修改 .vue 文件 的 script 部分, 都會爲 組件 生成一個 新的實例對象, 銷燬舊的實例對象。
若是咱們只修改了 .vue 文件 的 template 部分, 客戶端(即瀏覽器) 會進行 熱更新, 過程以下:
同上,服務端 經過 websocket 鏈接 通知 客戶端 更新;
同上, 客戶端 經過 動態添加script元素 的方式獲取 更新之後的打包文件;
同上, 安裝打包文件,即執行 新的打包文件 中的 js 代碼, 使用 打包文件中的 module 更新瀏覽器緩存的同名 module;
觸發經過 module.hot.accept 註冊的 callback;
執行 api 提供的 rerender 方法, 更新組件。
執行 rerender 方法時, 會先獲取 修改之後的template區域塊 對應的 render、staticRenderFns, 而後 更新原組件的 render、staticRenderFns, 而後執行 組件實例 的 $forceUpdate 方法來更新 組件(更新組件的時候, 會使用新的render方法, 生成新的vnode節點樹)。
若是咱們 同時 修改了 .vue 文件的 template、 script部分, 會按照上面 第一種狀況 進行 熱更新,而且不會觸發上面代碼中經過 module.hot.accept 註冊的 callback。
vue-style-loader 會在 打包代碼 中注入 熱更新 style區域塊 的代碼, 以下:
...
var add = require("!../node_modules/_vue-style-loader@4.1.2@vue-style-loader/lib/addStylesClient.js").default
var update = add("05835b6f", content, false, {});
// Hot Module Replacement
if(module.hot) {
// When the styles change, update the <style> tags
if(!content.locals) {
module.hot.accept("!!../node_modules/_css-loader@3.1.0@css-loader/dist/cjs.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/loaders/stylePostLoader.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&", function() {
// 當 .vue 文件的 styles 區域塊更改時, 客戶端執行這一段代碼
var newContent = require("!!../node_modules/_css-loader@3.1.0@css-loader/dist/cjs.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/loaders/stylePostLoader.js!../node_modules/_vue-loader@15.7.1@vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css&");
if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
// 執行update方法, 更新styles
update(newContent);
});
}
}
...
複製代碼
若是咱們修改了 .vue 文件 的 styles 區域塊,客戶端(即瀏覽器) 會進行 熱更新, 過程以下:
同上,服務端 經過 websocket 鏈接 通知 客戶端 更新;
同上,客戶端 經過 動態添加script元素 的方式獲取 更新之後的打包文件;
同上,安裝打包文件,即執行 新的打包文件 中的 js 代碼, 使用 打包文件中的 module 更新瀏覽器緩存的同名 module;
觸發經過 module.hot.accept 註冊的 callback;
執行 update 方法, 更新樣式。
更新樣式 的時候, 會先 移除原來的 style 標籤, 而後 添加新的 style 標籤。
若是 style 標籤 上有 module 屬性,除了 vue-style-loader 會注入 熱更新代碼 外,vue-loader 也會在 打包代碼 中注入 熱更新代碼,以下:
// 熱更新代碼
module.hot && module.hot.accept(["./App.vue?vue&type=style&index=1&id=7ba5bd90&module=true&scoped=true&lang=css&"], function () {
// 當.vue的style區域塊發生變化, 且style標籤有module屬性, 執行這一段邏輯
var oldLocals = cssModules["$style"]
if (oldLocals) {
// 獲取新的惟一類名、id名
var newLocals = require("./App.vue?vue&type=style&index=1&id=7ba5bd90&module=true&scoped=true&lang=css&")
if (JSON.stringify(newLocals) !== JSON.stringify(oldLocals)) {
// 更新vue實例的$style屬性
cssModules["$style"] = newLocals
// 執行vue實例的 $forceUpdate 方法,從新執行 render 方法
require("D:\\study\\demo\\webpack\\webpack-4-demo\\node_modules\\_vue-hot-reload-api@2.3.3@vue-hot-reload-api\\dist\\index.js").rerender("7ba5bd90")
}
}
})
複製代碼
執行上述 熱更新代碼, 會 更新 vue實例 的 $style 屬性, 而後觸發 vue 實例 的 $forceUpdate 方法, 從新渲染。
一個 style 區域塊 對應一個 style 標籤。修改某一個 style 區域塊 以後,會更新對應的 style 標籤。
style 區域塊 的 熱更新 和 template、script 區域塊的 熱更新 互不影響。
生產模式 下, webpack 默認啓用 tree shaking。若是此時項目 根目錄 中的 package.json 中的 sideEffects 的值爲 false,且 .vue 文件 的 style 標籤 沒有 module 屬性,使用 vue-loader 處理 .vue 文件 的時候, 會產生 樣式丟失 的狀況,即 styles 區域塊 不會添加到 head 中或者 被提取到公共的css文件中。
首先,先看一下 .vue 文件 通過處理之後生成的 js代碼, 以下:
// 從 template區域塊 獲取 render、 staticRenderFns 方法
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
// 從 script區域塊 獲取 組件的配置項對象
import scriptExports from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
// 獲取 styles區域塊的內容
import style0 from "./App.vue?vue&type=style&index=0&lang=css&"
// 獲取 styles(scoped)區域塊的內容
import style1 from "./App.vue?vue&type=style&index=1&id=7ba5bd90&scoped=true&lang=css&"
/* normalize component */
import normalizer from "!../node_modules/_vue-loader@15.7.1@vue-loader/lib/runtime/componentNormalizer.js"
// 返回構建組件須要的配置項對象, 包含 data、props、render、staticRenderFns 等
var component = normalizer(
scriptExports,
render,
staticRenderFns,
false,
null,
"7ba5bd90",
null
)
component.options.__file = "src/App.vue"
// 輸出組件完整的配置項
export default component.exports
複製代碼
在上面的代碼中,template 區域塊 返回的 render、staticRenderFns, script 區域塊 返回的 scriptExports, 都有被 normalizer 方法使用, 而 styles 區域塊 返回的 style0、style1 則沒有被使用。 在 打包代碼 的時候, tree shaking 就會自動移除 styles 區域塊 對應的代碼,致使 樣式丟失。
解決方法:
修改 package.json 文件中的 sideEffects 屬性, 告訴 webpack .vue 文件在使用 tree shaking 的時候會有 反作用, 以下:
"sideEffects": [
"*.vue"
]
複製代碼
有了上述配置, webpack 在處理 .vue 文件的時候, 不會使用 tree shaking, 不會出現樣式丟失的問題。
可是這種解決方法有一個問題, 若是 script 區域塊 中經過 import 的方式引入了 未使用的模塊,未使用的模塊在最後打包代碼的時候不會被刪除。
經過 rule.sideEffects 指定 具體的模塊 在使用 tree shaking 的時候會有 反作用, 以下:
// webpackConfig:
{
test: /\.css$/,
oneOf: [{
resourceQuery: /\?vue/,
// 指定.vue文件的 style區域塊 使用 tree shaking 時會有反作用
sideEffects: true,
use: [isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader', 'css-loader']
}, {
use: [isProduction ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader']
}]
},
{
test: /\.scss$/,
oneOf: [{
resourceQuery: /\?vue/,
// 指定.vue文件的 style(lang=scss)區域塊 使用 tree shaking 時會有反作用
sideEffects: true,
use: [isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader', 'css-loader', 'sass-loader']
}, {
use: [isProduction ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader', 'sass-loader']
}]
}
// package.json
{
sideEffects: false
}
複製代碼
上述配置, 明確說明了 .vue 文件 的 style 區域塊 在使用 tree shaking 的時候, 會有 反作用,在打包的時候不會刪除。
這樣的話,樣式不會丟失, 而且若是 script 區域塊 中經過 import 的方式引入了 未使用的模塊,未使用的模塊在最後打包代碼的時候會被刪除