商城系統(tǒng) 注冊(cè)

微信小程序打包插件開(kāi)發(fā)體驗(yàn)

2018-08-07|HiShop
導(dǎo)讀:微信官方已經(jīng)開(kāi)放了插件的共享,那么對(duì)于微信小程序開(kāi)發(fā)來(lái)說(shuō),如何做到小程序打包插件呢?下面來(lái)看看文章。...

微信官方已經(jīng)開(kāi)放了插件的共享,那么對(duì)于微信小程序開(kāi)發(fā)來(lái)說(shuō),如何做到小程序打包插件呢?下面來(lái)看看文章。

微信小程序打包插件開(kāi)發(fā)體驗(yàn)

如果你看過(guò)文檔,相信你一定知道:

  • 每個(gè)插件必須要有 apply 方法,用于 webpack 引擎執(zhí)行你想要執(zhí)行的代碼。
  • 兩個(gè)重要的對(duì)象 Compiler 和 Compilation,你可以在上面綁定事件鉤子(webpack 執(zhí)行到該步驟的時(shí)候調(diào)用),具體有哪些事件鉤子可以閱讀Compiler hooks。
  • module 和 chunk 的關(guān)系,我們可以理解為每個(gè)文件都會(huì)有一個(gè) module,而一個(gè) chunk 則是由多個(gè) module 來(lái)組成。
  • webpack 整個(gè)打包流程有那些事件
  • 如何寫(xiě)一個(gè)簡(jiǎn)單的 loader

如果感覺(jué)無(wú)從著手,可以繼續(xù)看看我是如何一步步開(kāi)發(fā)并完善 mini-program-webpack-loader 來(lái)打包小程序的。

小程序有一個(gè)固定的套路,首先需要有一個(gè) app.json 文件來(lái)定義所有的頁(yè)面路徑,然后每個(gè)頁(yè)面有四個(gè)文件組成:.js,.json,.wxml,.wxss。所以我以 app.json 作為 webpack entry,當(dāng) webpack 執(zhí)行插件的 apply 的時(shí)候,通過(guò)獲取 entry 來(lái)知道小程序都有哪些頁(yè)面。

 

這里使用了兩個(gè)插件 MultiEntryPlugin,SingleEntryPlugin。為什么要這樣做呢?因?yàn)?webpack 會(huì)根據(jù)你的 entry 配置(這里的 entry 不只是 webpack 配置里的 entry,import(), require.ensure() 都會(huì)生成一個(gè) entry)來(lái)決定生成文件的個(gè)數(shù),我們不希望把所有頁(yè)面的 js 打包到一個(gè)文件,需要使用 SingleEntryPlugin 來(lái)生成一個(gè)新的 entry module;而那些靜態(tài)資源,我們可以使用 MultiEntryPlugin 插件來(lái)處理,把這些文件作為一個(gè) entry module 的依賴,在 loader 中配置 file-loader 即可把靜態(tài)文件輸出。偽代碼如下:

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);
        })
    }
 }
復(fù)制代碼

當(dāng)然,如果像上面那樣做,你會(huì)發(fā)現(xiàn)最后會(huì)多出一個(gè) main.js,xxx.js(使用 MultiEntryPlugin 時(shí)填的名字),main.js 對(duì)應(yīng)的是配置的 entry 生成的文件,xxx.js 則是 MultiEntryPlugin 生成的。這些文件不是我們需要的,所以需要去掉他。如果熟悉 webpack 文檔,我們有很多地方可以修改最終打包出來(lái)的文件,如 compiler 的 emit 事件,compilation 的 optimizeChunks 相關(guān)的事件都可以實(shí)現(xiàn)。其本質(zhì)上就是去修改 compilation.assets 對(duì)象。

在 mini-program-webpack-loader 中就使用了 emit 事件來(lái)處理這種不需要輸出的內(nèi)容。

 

小程序打包當(dāng)然沒(méi)這么簡(jiǎn)單,還得支持wxml、wxss、wxs和自定義組件的引用,所以這個(gè)時(shí)候就需要一個(gè) loader 來(lái)完成了,loader 需要做的事情也非常簡(jiǎn)單 —— 解析依賴的文件,如 .wxml 需要解析 import 組件的 src,wxs 的 src,.wxss 需要解析 @import,wxs 的 require,最后在 loader 中使用 loadModule 方法添加即可。自定義組件一開(kāi)始在 add entry 步驟的時(shí)候直接獲取了,所以不需要 loader 來(lái)完成。

 

這樣做也沒(méi)什么問(wèn)題,可是開(kāi)發(fā)體驗(yàn)是比較差的,如再添加一個(gè)自定義組件,一個(gè)頁(yè)面,webpack 是無(wú)感知的,所以需要在頁(yè)面中的 .json 發(fā)生改變時(shí)檢查是不是新增了自定義組件或者新增了頁(yè)面。這個(gè)時(shí)候遇到一個(gè)問(wèn)題,自定義組件的 js 是不能通過(guò) addModule 的方式來(lái)添加的,因?yàn)樽远x組件的 js 必須作為獨(dú)立的入口文件。在 loader 中是做不了,所以嘗試把文件傳到 plugin 中(因?yàn)?plugin 先于 loader 執(zhí)行,所以是可以建立 loader 和 plugin 通信的)。

簡(jiǎn)單粗暴的方式:

// 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);
    }
}
復(fù)制代碼

但是...。文件是傳到 plugin 了,可是再使用 SingleEntryPlugin 時(shí)你會(huì)發(fā)現(xiàn),沒(méi)效果。因?yàn)樵?compiler make 之后 webpack 已經(jīng)不能感知新的 module 添加了,所以是沒(méi)有用的,這個(gè)時(shí)候就需要根據(jù)文檔猜,怎么樣才能讓 webpack 感知到新的 module,根據(jù)文檔中的事件做關(guān)鍵字查詢,可以發(fā)現(xiàn)在編譯完成的時(shí)候會(huì)調(diào)用 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);
    		});
    	});
    });
復(fù)制代碼

如果在這個(gè)事件鉤子返回一個(gè) true 值,則可以使 webpack 調(diào)用 compiler additionalPass 事件鉤子,嘗試在這里添加文件,果然是可以的。

 

當(dāng)然,小程序打包還有些不同的地方,比如分包,如何用好 splitchunk,就不在啰嗦了,當(dāng)你開(kāi)始以后你會(huì)發(fā)現(xiàn)有很多的方法來(lái)實(shí)現(xiàn)想要的效果。

插件開(kāi)發(fā)到這里差不多了,總的來(lái)說(shuō),webpack 就是變著花樣的回調(diào),當(dāng)你知道每個(gè)回調(diào)該做什么的時(shí)候,webpack 用起來(lái)就輕松了。明顯我不知道,因?yàn)樵陂_(kāi)發(fā)過(guò)程中遇到了一些問(wèn)題。

遇到的問(wèn)題

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

既然是工具,當(dāng)然需要做更多的事情,有贊的小程序那么復(fù)雜,如果支持 resolve alias,node_modules 可以使得項(xiàng)目更方便維護(hù),或許你會(huì)說(shuō)這不是 webpack 最基本的功能嗎,不是的,我們當(dāng)然是希望可以在任何文件中使用 alias,node_modules 支持的不僅僅是 js。當(dāng)然這樣做就意味著事情將變得復(fù)雜,首先就是獲取文件路徑,必須是異步的,因?yàn)樵?webpack 4 中 resolve 不再支持 sync。其次就是小程序的目錄名不能是 node_modules,這時(shí)就需要一種計(jì)算相對(duì)路徑的規(guī)則,還是相對(duì)打包輸出的,而不是相對(duì)當(dāng)前項(xiàng)目目錄。

2.多個(gè)小程序項(xiàng)目的合并

有贊從小程序來(lái)講,有微商城版,有零售版,以及公共版,其中大多基礎(chǔ)功能,業(yè)務(wù)都是相同的,當(dāng)然不能再每個(gè)小程序在開(kāi)發(fā)一次,所以這個(gè)工具具備合并多個(gè)小程序當(dāng)然是必須的。這樣的合并稍微又要比從 node_modules 中取文件復(fù)雜一些,因?yàn)樾枰WC多個(gè)小程序合并后的頁(yè)面是正確的,而且要保證路徑不變。

這兩個(gè)問(wèn)題的最終的解決方案既是以 webpack rootContext 的 src 目錄為基準(zhǔn)目錄,以該目錄所在路徑計(jì)算打包文件的絕對(duì)路徑,然后根據(jù)入口文件的 app.json 所在目錄的路徑計(jì)算出最終輸出路徑。

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

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

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

      if (fullPath !== compilerContext) {
        fullPath = join(fullPath, path)
      }
    }
    // 根據(jù) 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}路徑錯(cuò)誤:不應(yīng)該還包含 node_modules`)
    }

    /**
     * 如果有 node_modules 字符串,則去模塊名稱(chēng)
     * 如果 app.json 在 node_modules 中,那 path 不應(yīng)該包含 node_modules 
     */

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

    return path
  }
}
復(fù)制代碼

3.如何把子包單獨(dú)依賴的內(nèi)容打包到子包內(nèi)

解決這個(gè)問(wèn)題的方法是通過(guò) optimizeChunks 事件,在每個(gè) chunk 的依賴的 module 中添加這個(gè) chunk 的入口文件,然后在 splitChunk 的 test 配置中檢查 module 被依賴的數(shù)量。如果只有一個(gè),并且是被子包依賴,則打包到子包內(nèi)。

4.webpack 支持單文件失敗

這是一個(gè)未解決的問(wèn)題,當(dāng)嘗試使用 webpack 來(lái)支持單文件的時(shí)候,好像沒(méi)那么方便:

  • 單文件拆分為四個(gè)文件后,可以使用 emitFile 和 addDependency 來(lái)創(chuàng)建文件,但是創(chuàng)建的文件不會(huì)執(zhí)行 loader
  • 使用 loadModule 會(huì)因?yàn)槲募到y(tǒng)不存在該文件會(huì)報(bào)錯(cuò)

HiShop小程序工具提供多類(lèi)型商城/門(mén)店小程序制作,可視化編輯 1秒生成5步上線。通過(guò)拖拽、拼接模塊布局小程序商城頁(yè)面,所看即所得,只需要美工就能做出精美商城。更多小程序商店請(qǐng)查看:小程序商店

微信小程序打包插件開(kāi)發(fā)體驗(yàn)

 

電話咨詢 預(yù)約演示 0元開(kāi)店