<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 初探webpack之編寫loader

    初探webpack之編寫loader

    loader加載器是webpack的核心之一,其用于將不同類型的文件轉換為webpack可識別的模塊,即用于把模塊原內容按照需求轉換成新內容,用以加載非js模塊,通過配合擴展插件,在webpack構建流程中的特定時機注入擴展邏輯來改變構建結果,從而完成一次完整的構建。

    描述

    webpack是一個現代JavaScript應用程序的靜態模塊打包器module bundler,當webpack處理應用程序時,它會遞歸地構建一個依賴關系圖dependency graph,其中包含應用程序需要的每個模塊,然后將所有這些模塊打包成一個或多個bundle
    使用webpack作為前端構建工具通常可以做到以下幾個方面的事情:

    • 代碼轉換: TypeScript編譯成JavaScriptSCSS編譯成CSS等。
    • 文件優化: 壓縮JavaScriptCSSHTML代碼,壓縮合并圖片等。
    • 代碼分割: 提取多個頁面的公共代碼、提取首屏不需要執行部分的代碼讓其異步加載。
    • 模塊合并: 在采用模塊化的項目里會有很多個模塊和文件,需要構建功能把模塊分類合并成一個文件。
    • 自動刷新: 監聽本地源代碼的變化,自動重新構建、刷新瀏覽器頁面,通常叫做模塊熱替換HMR
    • 代碼校驗: 在代碼被提交到倉庫前需要校驗代碼是否符合規范,以及單元測試是否通過。
    • 自動發布: 更新完代碼后,自動構建出線上發布代碼并傳輸給發布系統。

    對于webpack來說,一切皆模塊,而webpack僅能處理出js以及json文件,因此如果要使用其他類型的文件,都需要轉換成webpack可識別的模塊,即jsjson模塊。也就是說無論什么后綴的文件例如pngtxtvue文件等等,都需要當作js來使用,但是直接當作js來使用肯定是不行的,因為這些文件并不符合js的語法結構,所以就需需要webpack loader來處理,幫助我們將一個非js文件轉換為js文件,例如css-loaderts-loaderfile-loader等等。

    在這里編寫一個簡單的webpack loader,設想一個簡單的場景,在這里我們關注vue2,從實例出發,在平時我們構建vue項目時都是通過編寫.vue文件來作為模塊的,這種單文件組件的方式雖然比較清晰,但是如果一個組件比較復雜的話,就會導致整個文件相當大。當然vue中給我們提供了在.vue文件中引用jscss的方式,但是這樣用起來畢竟還是稍顯麻煩,所以我們可以通過編寫一個webpack loader,在編寫代碼時將三部分即htmljscss進行分離,之后在loader中將其合并,再我們編寫的loader完成處理之后再交與vue-loader去處理之后的事情。當然,關注點分離不等于文件類型分離,將一個單文件分成多個文件也只是對于代碼編寫過程中可讀性的傾向問題,在這里我們重點關注的是編寫一個簡單的loader而不在于對于文件是否應該分離的探討。文中涉及到的所有代碼都在https://github.com/WindrunnerMax/webpack-simple-environment

    實現

    搭建環境

    在這里直接使用我之前的 初探webpack之從零搭建Vue開發環境 中搭建的簡單vue + ts開發環境,環境的相關的代碼都在https://github.com/WindrunnerMax/webpack-simple-environment中的webpack--vue-cli分支中,我們直接將其clone并安裝。

    git clone https://github.com/WindrunnerMax/webpack-simple-environment.git
    git checkout webpack--vue-cli
    yarn install --registry https://registry.npm.taobao.org/
    

    之后便可以通過運行yarn dev來查看效果,在這里我們先打印一下此時的目錄結構。

    webpack--vue-cli
    ├── dist
    │   ├── static
    │   │   └── vue-large.b022422b.png
    │   ├── index.html
    │   ├── index.js
    │   └── index.js.LICENSE.txt
    ├── public
    │   └── index.html
    ├── src
    │   ├── common
    │   │   └── styles.scss
    │   ├── components
    │   │   ├── tab-a.vue
    │   │   └── tab-b.vue
    │   ├── router
    │   │   └── index.ts
    │   ├── static
    │   │   ├── vue-large.png
    │   │   └── vue.jpg
    │   ├── store
    │   │   └── index.ts
    │   ├── views
    │   │   └── framework.vue
    │   ├── App.vue
    │   ├── index.ts
    │   ├── main.ts
    │   ├── sfc.d.ts
    │   └── sum.ts
    ├── LICENSE
    ├── README.md
    ├── babel.config.js
    ├── package.json
    ├── tsconfig.json
    ├── webpack.config.js
    └── yarn.lock
    

    編寫loader

    在編寫loader之前,我們先關注一下上邊目錄結構中的.vue文件,因為此時我們需要將其拆分,但是如何將其拆分是需要考慮一下的,為了盡量不影響正常的使用,在這里采用了如下的方案。

    • template部分留在了.vue文件中,因為一些插件例如Vetur是會檢查template中的一些語法,例如將其抽離出作為html文件,對于@click等語法prettier是會有error提醒的,而且如果不存在.vue文件的話,對于在TS中使用declare module "*.vue"也需要修改,所以本著最小影響的原則我們將template部分留在了.vue文件中,保存了.vue這個聲明的文件。
    • 對于script部分,我們將其抽出,如果是使用js編寫的,那么就將其命名為.vue.js,同樣ts編寫的就命名為.vue.ts
    • 對于style部分,我們將其抽出,與script部分采用同樣的方案,使用cssscssless也分別命名為.vue.css.vue.scss.vue.less,而對于scoped我們通過注釋的方式來實現。

    通過以上的修改,我們將文件目錄再次打印出來,重點關注于.vue文件的分離。

    webpack--loader
    ├── dist
    │   ├── static
    │   │   └── vue-large.b022422b.png
    │   ├── index.html
    │   ├── index.js
    │   └── index.js.LICENSE.txt
    ├── public
    │   └── index.html
    ├── src
    │   ├── common
    │   │   └── styles.scss
    │   ├── components
    │   │   ├── tab-a
    │   │   │   ├── tab-a.vue
    │   │   │   └── tab-a.vue.ts
    │   │   └── tab-b
    │   │       ├── tab-b.vue
    │   │       └── tab-b.vue.ts
    │   ├── router
    │   │   └── index.ts
    │   ├── static
    │   │   ├── vue-large.png
    │   │   └── vue.jpg
    │   ├── store
    │   │   └── index.ts
    │   ├── views
    │   │   └── framework
    │   │       ├── framework.vue
    │   │       ├── framework.vue.scss
    │   │       └── framework.vue.ts
    │   ├── App.vue
    │   ├── index.ts
    │   ├── main.ts
    │   ├── sfc.d.ts
    │   └── sum.ts
    ├── LICENSE
    ├── README.md
    ├── babel.config.js
    ├── package.json
    ├── tsconfig.json
    ├── vue-multiple-files-loader.js
    ├── webpack.config.js
    └── yarn.lock
    

    現在我們開始正式編寫這個loader了,首先需要簡單說明一下loader的輸入與輸出以及常用的模塊。

    • 簡單來說webpack loader是一個從stringstring的函數,輸入的是字符串的代碼,輸出也是字符串的代碼。
    • 通常來說對于各種文件的處理loader已經都有很好的輪子了,我們自己來編寫的loader通常是用來做代碼處理的,也就是說在loader中拿到source之后,我們將其轉換為AST樹,然后在這個AST上進行一些修改,之后再將其轉換為字符串代碼之后進行返回。
    • 從字符串到AST語法分析樹是為了得到計算機容易識別的數據結構,在webpack中自帶了一些工具,acorn是代碼轉AST的工具,estraverseAST遍歷工具,escodegen是轉換AST到字符串代碼的工具。
    • 既然loader是字符串到字符串的,那么在代碼轉換為AST處理之后需要轉為字符串,然后再傳遞到下一個loader,下一個loader可能又要進行相同的轉換,這樣還是比較耗費時間的,所以可以通過speed-measure-webpack-plugin進行速率打點,以及cache-loader來存儲AST
    • loader-utils是在loader中常用的輔助類,常用的有urlToRequest絕對路徑轉webpack請求的相對路徑,urlToRequest來獲取配置loader時傳遞的參數。

    由于我們在這里這個需求是用不到AST相關的處理的,所以還是比較簡單的一個實例,首先我們需要寫一個loader文件,然后配置在webpack.config.js中,在根目錄我們建立一個vue-multiple-files-loader.js,然后在webpack.config.jsmodule.rule部分找到test: /\.vue$/,將這部分修改為如下配置。

    // ...
    {
        test: /\.vue$/,
        use: [
            "vue-loader",
            {
                loader: "./vue-multiple-files-loader",
                options: {
                    // 匹配的文件拓展名
                    style: ["scss", "css"],
                    script: ["ts"],
                },
            },
        ],
    }
    // ...
    

    首先可以看到在"vue-loader"之后我們編寫了一個對象,這個對象的loader參數是一個字符串,這個字符串是將來要被傳遞到require當中的,也就是說在webpack中他會自動幫我們把這個模塊requirerequire("./vue-multiple-files-loader")webpack loader是有優先級的,在這里我們的目標是首先經由vue-multiple-files-loader這個loader將代碼處理之后再交與vue-loader進行處理,所以我們要將vue-multiple-files-loader寫在vue-loader后邊,這樣就會首先使用vue-multiple-files-loader代碼了。我們通過options這個對象傳遞參數,這個參數可以在loader中拿到。
    關于webpack loader的優先級,首先定義loader配置的時候,除了loaderoptions選項,還有一個enforce選項,其可接受的參數分別是pre: 前置loadernormal: 普通loaderinline: 內聯loaderpost: 后置loader,其優先級也是pre > normal > inline > post,那么相同優先級的loader就是從右到左、從下到上,從上到下很好理解,至于從右到左,只是webpack選擇了compose方式,而不是pipe的方式而已,在技術上實現從左往右也不會有難度,就是函數式編程中的兩種組合方式而已。此外,我們在require的時候還可以跳過某些loader!跳過normal loader-!跳過prenormal loader!!跳過pre normalpost loader,比如require("!!raw!./script.coffee"),關于loader的跳過,webpack官方的建議是,除非從另一個loader處理生成的,一般不建議主動使用。

    現在我們已經處理好vue-multiple-files-loader.js這個文件的創建以及loader的引用了,那么我們可以通過他來編寫代碼了,通常來說,loader一般是比較耗時的應用,所以我們通過異步來處理這個loader,通過this.async告訴loader-runner這個loader將會異步地回調,當我們處理完成之后,使用其返回值將處理后的字符串代碼作為參數執行即可。

    module.exports = async function (source) {
        const done = this.async();
        // do something
        done(null, source);
    }
    

    對于文件的操作,我們使用promisify來處理,以便我們能夠更好地使用async/await

    const fs = require("fs");
    const { promisify } = require("util");
    
    const readDir = promisify(fs.readdir);
    const readFile = promisify(fs.readFile);
    

    下面我們回到上邊的需求上來,思路很簡單,首先我們在這個loader中僅會收到以.vue結尾的文件,這是在webpack.config.js中配置的,所以我們在這里僅關注.vue文件,那么在這個文件下,我們需要獲取這個文件所在的目錄,然后將其遍歷,通過webpack.config.js中配置的options來構建正則表達式去匹配同級目錄下的scriptstyle的相關文件,對于匹配成功的文件我們將其讀取然后按照.vue文件的規則拼接到source中,然后將其返回之后將代碼交與vue-loader處理即可。
    那么我們首先處理一下當前目錄,以及當前處理的文件名,還有正則表達式的構建,在這里我們傳遞了scsscssts,那么對于App.vue這個文件來說,將會構建/App\.vue\.css$|App\.vue\.scss$/App\.vue\.ts$這兩個正則表達式。

    const filePath = this.context;
    const fileName = this.resourcePath.replace(filePath + "/", "");
    
    const options = loaderUtils.getOptions(this) || {};
    const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
    const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));
    

    之后我們通過遍歷目錄的方式,來匹配符合要求的scriptstyle的文件路徑。

    let stylePath = null;
    let scriptPath = null;
    
    const files = await readDir(filePath);
    files.forEach(file => {
        if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
        if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
    });
    

    之后對于script部分,存在匹配節點且原.vue文件不存在script標簽,則異步讀取文件之后將代碼進行拼接,如果拓展名不為js的話,例如是ts編寫的那么就會將其作為lang="ts"去處理,之后將其拼接到source這個字符串中。

    if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
        const extName = scriptPath.split(".").pop();
        if (extName) {
            const content = await readFile(scriptPath, "utf8");
            const scriptTagContent = [
                "<script ",
                extName === "js" ? "" : `lang="${extName}" `,
                ">\n",
                content,
                "</script>",
            ].join("");
            source = source + "\n" + scriptTagContent;
        }
    }
    

    之后對于style部分,存在匹配節點且原.vue文件不存在style標簽,則異步讀取文件之后將代碼進行拼接,如果拓展名不為css的話,例如是scss編寫的那么就會將其作為lang="scss"去處理,如果代碼中存在單行的// scoped字樣的話,就會將這個style部分作scoped處理,之后將其拼接到source這個字符串中。

    if (stylePath && !/<style[\s\S]*?>/.test(source)) {
        const extName = stylePath.split(".").pop();
        if (extName) {
            const content = await readFile(stylePath, "utf8");
            const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
            const styleTagContent = [
                "<style ",
                extName === "css" ? "" : `lang="${extName}" `,
                scoped ? "scoped " : " ",
                ">\n",
                content,
                "</style>",
            ].join("");
            source = source + "\n" + styleTagContent;
        }
    }
    

    在之后使用done(null, source)觸發回調完成loader的流程,相關代碼如下所示,完整代碼在https://github.com/WindrunnerMax/webpack-simple-environment中的webpack--loader分支當中。

    const fs = require("fs");
    const path = require("path");
    const { promisify } = require("util");
    const loaderUtils = require("loader-utils");
    
    const readDir = promisify(fs.readdir);
    const readFile = promisify(fs.readFile);
    
    module.exports = async function (source) {
        const done = this.async();
        const filePath = this.context;
        const fileName = this.resourcePath.replace(filePath + "/", "");
    
        const options = loaderUtils.getOptions(this) || {};
        const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
        const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));
    
        let stylePath = null;
        let scriptPath = null;
    
        const files = await readDir(filePath);
        files.forEach(file => {
            if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
            if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
        });
    
        // 存在匹配節點且原`.vue`文件不存在`script`標簽
        if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
            const extName = scriptPath.split(".").pop();
            if (extName) {
                const content = await readFile(scriptPath, "utf8");
                const scriptTagContent = [
                    "<script ",
                    extName === "js" ? "" : `lang="${extName}" `,
                    ">\n",
                    content,
                    "</script>",
                ].join("");
                source = source + "\n" + scriptTagContent;
            }
        }
    
        // 存在匹配節點且原`.vue`文件不存在`style`標簽
        if (stylePath && !/<style[\s\S]*?>/.test(source)) {
            const extName = stylePath.split(".").pop();
            if (extName) {
                const content = await readFile(stylePath, "utf8");
                const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
                const styleTagContent = [
                    "<style ",
                    extName === "css" ? "" : `lang="${extName}" `,
                    scoped ? "scoped " : " ",
                    ">\n",
                    content,
                    "</style>",
                ].join("");
                source = source + "\n" + styleTagContent;
            }
        }
    
        // console.log(stylePath, scriptPath, source);
        done(null, source);
    };
    

    每日一題

    https://github.com/WindrunnerMax/EveryDay
    

    參考

    https://webpack.js.org/api/loaders/
    https://juejin.cn/post/6844904054393405453
    https://segmentfault.com/a/1190000014685887
    https://segmentfault.com/a/1190000021657031
    https://webpack.js.org/concepts/loaders/#inline
    http://t.zoukankan.com/hanshuai-p-11287231.html
    https://v2.vuejs.org/v2/guide/single-file-components.html
    
    posted @ 2022-05-05 09:26  WindrunnerMax  閱讀(184)  評論(0編輯  收藏  舉報
    国产美女a做受大片观看