使用 webpack 構建小程序項目

本文內容

  • 如何開發 webpack 插件及開發小程序打包插件的過程
  • 在開發過程當中遇到的問題以及如何解決的
  • mini-program-webpack-loader 能夠作什麼

開發 webpack 插件

相信開發過插件的同窗,都看過 Writing a Plugin 或相似的文章,由於 mini-program-webpack-loader 這個工具開發時正好 webpack 4 發佈了,因此就閱讀了這篇文章,順便看了如下幾篇文檔。javascript

若是你看過文檔,相信你必定知道:java

  • 每一個插件必需要有 apply 方法,用於 webpack 引擎執行你想要執行的代碼。
  • 兩個重要的對象 Compiler 和 Compilation,你能夠在上面綁定事件鉤子(webpack 執行到該步驟的時候調用),具體有哪些事件鉤子能夠閱讀 Compiler hooks
  • module 和 chunk 的關係,咱們能夠理解爲每一個文件都會有一個 module,而一個 chunk 則是由多個 module 來組成。
  • webpack 整個打包流程有那些事件
  • 如何寫一個簡單的 loader

若是感受無從着手,能夠繼續看看我是如何一步步開發並完善 mini-program-webpack-loader 來打包小程序的。node

小程序有一個固定的套路,首先須要有一個 app.json 文件來定義全部的頁面路徑,而後每一個頁面有四個文件組成:.js,.json,.wxml,.wxss。因此我以 app.json 做爲 webpack entry,當 webpack 執行插件的 apply 的時候,經過獲取 entry 來知道小程序都有哪些頁面。大概流程像下面一張圖,一個小程序打包插件差很少就這樣完成了。webpack

這裏使用了兩個插件 MultiEntryPlugin,SingleEntryPlugin。爲何要這樣作呢?由於 webpack 會根據你的 entry 配置(這裏的 entry 不僅是 webpack 配置裏的 entry,import(), require.ensure() 都會生成一個 entry)來決定生成文件的個數,咱們不但願把全部頁面的 js 打包到一個文件,須要使用 SingleEntryPlugin 來生成一個新的 entry module;而那些靜態資源,咱們可使用 MultiEntryPlugin 插件來處理,把這些文件做爲一個 entry module 的依賴,在 loader 中配置 file-loader 便可把靜態文件輸出。僞代碼以下:git

const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin');
 const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
 
 class MiniPlugin {
    apply (compiler) {
        let options = compiler.options
        let context = compiler.rootContext
        let entry = options.entry
        let files = loadFiles(entry)
        let scripts = files.filter(file => /\.js$/.test(file))
        let assets = files.filter(file => !/\.js$/.test(file))
        
       new  MultiEntryPlugin(context, assets, '__assets__').apply(compiler)
        
        scripts.forEach((file => {
            let fileName = relative(context, file).replace(extname(file), '');
            new SingleEntryPlugin(context, file, fileName).apply(compiler);
        })
    }
 }
複製代碼

固然,若是像上面那樣作,你會發現最後會多出一個 main.js,xxx.js(使用 MultiEntryPlugin 時填的名字),main.js 對應的是配置的 entry 生成的文件,xxx.js 則是 MultiEntryPlugin 生成的。這些文件不是咱們須要的,因此須要去掉他。若是熟悉 webpack 文檔,咱們有不少地方能夠修改最終打包出來的文件,如 compiler 的 emit 事件,compilation 的 optimizeChunks 相關的事件均可以實現。其本質上就是去修改 compilation.assets 對象。github

在 mini-program-webpack-loader 中就使用了 emit 事件來處理這種不須要輸出的內容。大概流程就像下面這樣: web

小程序打包固然沒這麼簡單,還得支持wxml、wxss、wxs和自定義組件的引用,因此這個時候就須要一個 loader 來完成了,loader 須要作的事情也很是簡單 —— 解析依賴的文件,如 .wxml 須要解析 import 組件的 src,wxs 的 src,.wxss 須要解析 @import,wxs 的 require,最後在 loader 中使用 loadModule 方法添加便可。自定義組件一開始在 add entry 步驟的時候直接獲取了,因此不須要 loader 來完成。這個時候的圖:npm

這樣作也沒什麼問題,但是開發體驗是比較差的,如再添加一個自定義組件,一個頁面,webpack 是無感知的,因此須要在頁面中的 .json 發生改變時檢查是否是新增了自定義組件或者新增了頁面。這個時候遇到一個問題,自定義組件的 js 是不能經過 addModule 的方式來添加的,由於自定義組件的 js 必須做爲獨立的入口文件。在 loader 中是作不了,因此嘗試把文件傳到 plugin 中(由於 plugin 先於 loader 執行,因此是能夠創建 loader 和 plugin 通訊的)。簡單粗暴的方式:json

// loader.js
class MiniLoader {}

module.exports = function (content) {
    new MiniLoader(this, content)
}
module.exports.$applyPluginInstance = function (plugin) {
  MiniLoader.prototype.$plugin = plugin
}

// plugin.js
const loader = require('./loader')
class MiniPlugin {
    apply (compiler) {
        loader.$applyPluginInstance(this);
    }
}
複製代碼

可是...。文件是傳到 plugin 了,但是再使用 SingleEntryPlugin 時你會發現,沒效果。由於在 compiler make 以後 webpack 已經不能感知新的 module 添加了,因此是沒有用的,這個時候就須要根據文檔猜,怎麼樣才能讓 webpack 感知到新的 module,根據文檔中的事件作關鍵字查詢,能夠發如今編譯完成的時候會調用 compilation needAdditionalPass 事件鉤子:小程序

this.emitAssets(compilation, err => {
    	if (err) return finalCallback(err);
    
    	if (compilation.hooks.needAdditionalPass.call()) {
    		compilation.needAdditionalPass = true;
    
    		const stats = new Stats(compilation);
    		stats.startTime = startTime;
    		stats.endTime = Date.now();
    		this.hooks.done.callAsync(stats, err => {
    			if (err) return finalCallback(err);
    
    			this.hooks.additionalPass.callAsync(err => {
    				if (err) return finalCallback(err);
    				this.compile(onCompiled);
    			});
    		});
    		return;
    	}
    
    	this.emitRecords(err => {
    		if (err) return finalCallback(err);
    
    		const stats = new Stats(compilation);
    		stats.startTime = startTime;
    		stats.endTime = Date.now();
    		this.hooks.done.callAsync(stats, err => {
    			if (err) return finalCallback(err);
    			return finalCallback(null, stats);
    		});
    	});
    });
複製代碼

若是在這個事件鉤子返回一個 true 值,則可使 webpack 調用 compiler additionalPass 事件鉤子,嘗試在這裏添加文件,果真是能夠的。這個時候的圖就成了這樣:

固然,小程序打包還有些不一樣的地方,好比分包,如何用好 splitchunk,就不在囉嗦了,當你開始之後你會發現有不少的方法來實現想要的效果。

插件開發到這裏差很少了,總的來講,webpack 就是變着花樣的回調,當你知道每一個回調該作什麼的時候,webpack 用起來就輕鬆了。明顯我不知道,由於在開發過程當中遇到了一些問題。

遇到的問題

1.如何在小程序代碼中支持 resolve alias,node_modules?

既然是工具,固然須要作更多的事情,有讚的小程序那麼複雜,若是支持 resolve alias,node_modules 可使得項目更方便維護,或許你會說這不是 webpack 最基本的功能嗎,不是的,咱們固然是但願能夠在任何文件中使用 alias,node_modules 支持的不只僅是 js。固然這樣作就意味着事情將變得複雜,首先就是獲取文件路徑,必須是異步的,由於在 webpack 4 中 resolve 再也不支持 sync。其次就是小程序的目錄名不能是 node_modules,這時就須要一種計算相對路徑的規則,仍是相對打包輸出的,而不是相對當前項目目錄。

2.多個小程序項目的合併

有贊從小程序來說,有微商城版,有零售版,以及公共版,其中大多基礎功能,業務都是相同的,固然不能再每一個小程序在開發一次,因此這個工具具有合併多個小程序固然是必須的。這樣的合併稍微又要比從 node_modules 中取文件複雜一些,由於須要保證多個小程序合併後的頁面是正確的,並且要保證路徑不變。

這兩個問題的最終的解決方案既是以 webpack rootContext 的 src 目錄爲基準目錄,以該目錄所在路徑計算打包文件的絕對路徑,而後根據入口文件的 app.json 所在目錄的路徑計算出最終輸出路徑。

exports.getDistPath = (compilerContext, entryContexts) => {
  /** * webpack 以 config 所在目錄的 src 爲打包入口 * 因此能夠根據該目錄追溯源文件地址 */
  return (path) => {
    let fullPath = compilerContext
    let npmReg = /node_modules/g
    let pDirReg = /^[_|\.\.]\//g

    if (isAbsolute(path)) {
      fullPath = path
    } else {
      // 相對路徑:webpack 最後生成的路徑,打包入口外的文件都以 '_' 表示上級目錄

      while (pDirReg.test(path)) {
        path = path.substr(pDirReg.lastIndex)
        fullPath = join(fullPath, '../')
      }

      if (fullPath !== compilerContext) {
        fullPath = join(fullPath, path)
      }
    }
    // 根據 entry 中定義的 json 文件目錄獲取打包後所在目錄,若是不能獲取就返回原路徑
    let contextReg = new RegExp(entryContexts.join('|'), 'g')

    if (fullPath !== compilerContext && contextReg.exec(fullPath)) {
      path = fullPath.substr(contextReg.lastIndex + 1)
      console.assert(!npmReg.test(path), `文件${path}路徑錯誤:不該該還包含 node_modules`)
    }

    /** * 若是有 node_modules 字符串,則去模塊名稱 * 若是 app.json 在 node_modules 中,那 path 不該該包含 node_modules */

    if (npmReg.test(path)) {
      path = path.substr(npmReg.lastIndex + 1)
    }

    return path
  }
}
複製代碼

3.如何把子包單獨依賴的內容打包到子包內

解決這個問題的方法是經過 optimizeChunks 事件,在每一個 chunk 的依賴的 module 中添加這個 chunk 的入口文件,而後在 splitChunk 的 test 配置中檢查 module 被依賴的數量。若是隻有一個,而且是被子包依賴,則打包到子包內。

4.webpack 支持單文件失敗

這是一個未解決的問題,當嘗試使用 webpack 來支持單文件的時候,好像沒那麼方便:

  • 單文件拆分爲四個文件後,可使用 emitFile 和 addDependency 來建立文件,可是建立的文件不會執行 loader
  • 使用 loadModule 會由於文件系統不存在該文件會報錯

寫在最後

最後固然是介紹 mini-program-webpack-loader 能夠作什麼了。

該工具主要解決如下問題:

  • 小程序不支持 npm
  • 目錄嵌套太深,路徑難以管理
  • 舊項目太大,想要使用新工具成本過高
  • 提供大型小程序項目中常規優化建議

重複一遍

  • 你能夠直接使用 npm 來下載 zanui-weapp
  • 你能夠告別 「../../../../../xxx」 了
  • 你可使用 mpVue,taro 來寫新功能了,不用擔憂不兼容

最後的最後留下文檔地址:mini-program-webpack-loader

相關文章
相關標籤/搜索