寫在前面
為了更好的説明,我們模仿Vue.js開發了一個類似的簡化版本的前端框架Quick Paper(文檔) 來幫助你理解一些細節。因此在開始之前,讓我們先大致瞭解一下此項目的結構,方便後續描述。
温馨提示:我們推薦你在開始之前去Github上把此項目clone下來後,對照着源碼進行學習!
目錄結構
其實你只需要關注下面四個文件夾:
- src:框架源碼;
- loader:類似vue-loader,用來解析.paper文件的loader;
- style-loader:和上面的一樣,只不過這個是用來解析樣式文件的(包括.css文件和.paper文件中的style標籤部分);
- loader-plug:一些輔助功能,比如校對webpack的一些配置。
框架源碼
接着,我們對源碼src部分的目錄結構再稍微展開一下(因為我們這裏的重點不是源碼部分,而是那些loader或plug是如何配置完成一系列解析工作的,因此源碼部分就在下面簡單的説明就點到為止)。
-
core:框架對象的基礎代碼
- global-api:給框架對象掛載的全局方法
-
instance:框架對象
- index.js:框架對象運行入口
- init.js:負責對象的初始化相關工作
- lifecycle.js:負責對象的生命週期管理
- render.js:對象的渲染啓動等方面的任務
- observe:監聽數據改變方法(被框架對象使用)
- vnode:虛擬DOM相關代碼(被框架對象使用)
- module:為框架對象擴展內置指令,組件等的地方
- tools:一些工具方法,因為複用性和方便管理,集中寫在一起
- index.js:打包入口文件,也就是這份文件把所有的資源整合成一個完整的框架
所以從上面的代碼就可以看出來,文件src/core/instance/index.js是對象本身,從這個文件開始開即可!
如果有什麼不清楚的,可以去issue給我們留言。
Loader和執行順序
對於我們用於學習的項目Quick Paper而言,我們是把代碼整合到文件.paper中去,文件結構大致如下:
<template>
<!-- 頁面的元素在這裏 -->
</template>
<script>
// 邏輯控制代碼在這裏
export default {
};
</script>
<style>
/* 在這裏編輯樣式代碼 */
</style>
你想,我們使用webpack打包項目的時候,他是不可能認識.paper文件的,當然就無法知道如何解析上面這份文件了,而開發一個loader用以解析上面的文件,就是這裏要説明的。
loader
在説明loader之前,我們先要看看我們編輯的.paper是如何被我們使用的。因為如何使用就決定了我們需要如何解析。
和vue類似,先假設我們有一個App.paper文件:
import App from './App.paper';
new QuickPaper({
render:createElement => createElement(App),
// ...
});
因為render裏面只記錄了頁面內容,可是.paper文件裏面可是記錄了頁面內容+邏輯控制+頁面樣式的。其餘的內容怎麼辦?
// 導入js [邏輯控制]
import script from './${filename}?QuickPaper&type=script&lang=js&hash=${id}&';
// 導入css [頁面樣式]
import './${filename}?QuickPaper&type=style&lang=css&hash=${id}&';
script.render=${code};
// [頁面內容]
export default script;
可以看出來,頁面內容直接默認導出後給render配置項即可,別的內容因為新增了導入語句,會觸發對應的loader進行解析,也就是説,這裏其實可以分為兩步:
- 第一步:對於未考慮到的內容執行新的導入語句,觸發對應的loader解析
- 第二步:導出render需要的內容
style-loader
比如頁面樣式部分的導入語句:
import './${filename}?QuickPaper&type=style&lang=css&hash=${id}&';
我們是如何讓webpack知道這是一個樣式文件,並且是使用css還是scss或別的loader來解析的,這屬於插件需要説明的部分。在此之前,我們還需要先説明一下樣式loader的工作原理。
為什麼樣式loader比較特殊?
根據返回值類型,可以把loader分成兩種:一種是返回js代碼(也就是一個模塊的代碼,有類似module.export語句)的loader,一個是不能作為最左邊loader的其他loader(比如返回一個CSS字符串)。
我們來看看我們webpack裏面是如何配置css的loader的:
{
test: /\.css$/,
loader: ['quick-paper/style-loader/index.js', 'css-loader', 'postcss-loader']
}
這裏的重點是css-loader,他屬於第一種,返回js代碼的loader,對於我們自定義的'quick-paper/style-loader/index.js'而言,如果讓loader按照從右往左的順序執行,很難拿到真正的css代碼。
執行順序(loader和picth)
在説明如何解決上個問題前,我們需要先説明一下loader的picth和執行順序。
比如上面配置的三個loader而言,執行順序分為Pitch階段和Normal階段(可以理解為loader本身的行為):
- Pitch階段:'quick-paper/style-loader/index.js'->'css-loader'->'postcss-loader'
- Normal階段:'postcss-loader'->'css-loader'->'quick-paper/style-loader/index.js'
有一個特點是,在Pitch階段,如果某個loader有返回值,就會停止後續執行。
温馨提示:停止執行的意思是,在其右邊的loader,包括自己都執行完畢了(Pitch階段和Normal階段都結束了),返回的值會返回給前一個loader(Normal階段)!
如何實現?
這裏,我們就可以藉助給'quick-paper/style-loader/index.js'設置一個有返回值的Pitch來實現。
看看代碼結構:
// quick-paper/style-loader/index.js
const loaderApi = () => { };
loaderApi.pitch = function (remainingRequest) {
// request = ""!!../../node_modules/css-loader/dist/cjs.js!../../node_modules/postcss-loader/src/in...
let request = loaderUtils.stringifyRequest(this, '!!' + remainingRequest)
return `
// 獲取真正的css內容
var content = require(' + request + ');
// 然後調用方法添加到頁面中生效
require('./addStylesClient.js')(content);
`;
};
module.exports = loaderApi;
我們在'quick-paper/style-loader/index.js'中定義了Picth方法,在此方法裏面,返回了一個js字符串,項目運行的時候會運行這段字符串,這段字符串的意義就是調用樣式loader獲取真正的css後,運行addStylesClient.js方法使得在頁面生效。
温馨提示:關於addStylesClient.js方法請直接查看項目源碼,很容易讀懂,給樣式添加hash值讓scope生效,就是這個方法裏。
插件的作用和一些技巧
我們這裏來解釋一下,一個.paper文件拆分以後,如何讓對應的loader來進行解析。
插件的執行時機
首先需要理解,什麼是插件?
你可以這樣理解:如果説loader幫助webpack獲得解析更多類型文件,那插件就是一個打雜工,前者有專門的分工,後者是在特殊情況下幫助,而不是針對某個文件。
比如你可以在每次打包前調用一個查看刪除上次打包的結果,或者在打包失敗的時候重置一些參數,或者是別的一些操作等。
如何實現?
那麼,我們這裏需要插件幹什麼?
別忘了我們的需求是(拿css舉例子),如果遇到:
import './${filename}?QuickPaper&type=style&lang=css&hash=${id}&';
這樣的導入語句,我們工具lang=css發現是一個樣式文件,應該交給專門解析css的loader處理,當然,我們可以主動修改webpack的配置:
{
test: /type=style&lang=css/,
loader: ['quick-paper/style-loader/index.js', 'css-loader', 'postcss-loader']
}
可是,為了更簡單,我們可以通過插件,在每次打包前對loader配置進行修改(當然,也包括js等相關項),如此,便實現了。