站長資訊網
        最全最豐富的資訊網站

        實戰篇—微信小程序工程化探索之webpack

        實戰篇---微信小程序工程化探索之webpack

        相關學習推薦:微信小程序教程

        前言

        微信小程序因為其便捷的使用方式,以極快的速度傳播開來吸引了大量的使用者。市場需求急劇增加的情況下,每家互聯網企業都想一嘗甜頭,因此掌握小程序開發這一技術無疑是一名前端開發者不可或缺的技能。但小程序開發當中總有一些不便一直讓開發者詬病不已,主要表現在:

        • 初期缺乏方便的npm包管理機制(現階段確實可以使用npm包,但是操作確實不便)
        • 不能使用預編譯語言處理樣式
        • 無法通過腳本命令切換不同的開發環境,需手動修改對應環境所需配置(常規項目至少具備開發與生產環境)
        • 無法將規范檢查工具結合到項目工程中(諸如EsLint、StyleLint的使用)

        有了不少的問題之后,我開始思考如何將現代的工程化技術與小程序相結合。初期在社區中查閱資料時,許多前輩都基于gulp去做了不少實踐,對于小程序這種多頁應用來說gulp的流式工作方式似乎更加方便。在實際的實踐過后,我不太滿意應用gulp這一方案,所以我轉向了對webpack的實踐探索。我認為選擇webpack作為工程化的支持,盡管它相對gulp更難實現,但在未來的發展中一定會有非凡的效果,

        實踐

        我們先不考慮預編譯、規范等等較為復雜的問題,我們的第一個目標是如何應用webpack將源代碼文件夾下的文件輸出到目標文件夾當中,接下來我們就一步步來創建這個工程項目:

        /* 創建項目 */$ mkdir wxmp-base$ cd ./wxmp-base/* 創建package.json */$ npm init/* 安裝依賴包 */$ npm install webpack webpack-cli --dev復制代碼

        安裝好依賴之后我們為這個項目創建基礎的目錄結構,如圖所示:

        實戰篇---微信小程序工程化探索之webpack

        上圖所展示的是一個最簡單的小程序,它只包含app全局配置文件和一個home頁面。接下來我們不管全局或是頁面,我們以文件類型劃分為需要待加工的js類型文件和不需要再加工可以直接拷貝的wxmlwxssjson文件。以這樣的思路我們開始編寫供webpack執行的配置文件,在項目根目錄下創建一個build目錄存放webpack.config.js文件。

        $ mkdir build$ cd ./build$ touch webpack.config.js復制代碼
        /** webpack.config.js */const path = require('path');const CopyPlugin = require('copy-webpack-plugin');const ABSOLUTE_PATH = process.cwd();module.exports = {  context: path.resolve(ABSOLUTE_PATH, 'src'),  entry: {    app: './app.js',    'pages/home/index': './pages/home/index.js'   },  output: {    filename: '[name].js',    path: path.resolve(ABSOLUTE_PATH, 'dist')   },  module: {    rules: [       {        test: /.js$/,        exclude: /node_modules/,        use: {          loader: 'babel-loader',          options: {            presets: ['@babel/preset-env'],            plugins: ['@babel/plugin-transform-runtime'],           },         },       }     ]   },  plugins: [    new CopyPlugin([       {        from: '**/*.wxml',        toType: 'dir',       },       {        from: '**/*.wxss',        toType: 'dir',       },       {        from: '**/*.json',        toType: 'dir',       }     ])   ] };復制代碼

        在編寫完上述代碼之后,為大家解釋一下上述的代碼究竟會做些什么:

        1. 入口entry對象中我寫了兩個屬性,意在將app.jshome/index.js作為webpack的構建入口,它會以這個文件為起始點創建各自的依賴關系,這樣當我們在入口文件中引入其他文件時,被引入的文件也能被webpack所處理。
        2. module中我使用了babel-loaderjs文件進行ES6轉換為ES5的處理,并且加入了對新語法的處理,這樣我們就解決了在原生小程序開發中總是要反復引入regenerator-runtime的問題。(這一步我們需要安裝@babel/core@babel/preset-env@babel/plugin-transform-runtime@babel/runtimebabel-loader這幾個依賴包)
        3. 使用copy-webpack-plugin來處理不需要再加工的文件,這個插件可以直接將文件復制到目標目錄當中。

        我們了解完這些代碼的實際作用之后就可以在終端中運行webpack --config build/webpack.config.js命令。webpack會將源代碼編譯到dist文件夾中,這個文件夾中的內容就可用在開發者工具中運行、預覽、上傳。

        優化

        完成了最基礎的webpack構建策略后,我們實現了apphome頁面的轉化,但這還遠遠不夠。我們還需要解決許多的問題:

        • 頁面文件增多怎么辦,組件怎么處理
        • 預期的預編譯如何做
        • 規范如何結合到工程中
        • 環境變量怎么處理

        接下來我們針對以上幾點進行webpack策略的升級:

        頁面與組件

        一開始我的實現方法是寫一個工具函數利用glob收集pages和components下的js文件然后生成入口對象傳遞給entry。但是在實踐過程中,我發現這樣的做法有兩個弊端:

        1. 當終端中已經啟動了命令,這時候新增頁面或組件都不會自動生成新的入口,也就是我們要重跑一遍命令。
        2. 工具函數寫死了匹配pages和components文件夾下的文件,不利于項目的延展性,如果我們需要分包或者文件夾命名需要改動時,我們就需要改動工具函數。

        本著程序員應該是極度慵懶,能交給機器完成的事情絕不自己動手的信條,我開始研究新的入口生成方案。最終確定下來編寫一個webpack的插件,在webpack構建的生命周期中生成入口,廢話不多說上代碼:

        /** build/entry-extract-plugin.js */const fs = require('fs');const path = require('path');const chalk = require('chalk');const replaceExt = require('replace-ext');const { difference } = require('lodash');const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');const MultiEntryPlugin = require('webpack/lib/MultiEntryPlugin');class EntryExtractPlugin {  constructor() {    this.appContext = null;    this.pages = [];    this.entries = [];   }  /**   	*	收集app.json文件中注冊的pages和subpackages生成一個待處理數組   	*/   getPages() {    const app = path.resolve(this.appContext, 'app.json');    const content = fs.readFileSync(app, 'utf8');    const { pages = [], subpackages = [] } = JSON.parse(content);    const { length: pagesLength } = pages;    if (!pagesLength) {      console.log(chalk.red('ERROR in "app.json": pages字段缺失'));       process.exit();     }    /** 收集分包中的頁面 */     const { length: subPackagesLength } = subpackages;    if (subPackagesLength) {       subpackages.forEach((subPackage) => {        const { root, pages: subPages = [] } = subPackage;        if (!root) {          console.log(chalk.red('ERROR in "app.json": 分包配置中root字段缺失'));           process.exit();         }        const { length: subPagesLength } = subPages;        if (!subPagesLength) {          console.log(chalk.red(`ERROR in "app.json": 當前分包 "${root}" 中pages字段為空`));           process.exit();         }         subPages.forEach((subPage) => pages.push(`${root}/${subPage}`));       });     }    return pages;   }  /**   	*	以頁面為起始點遞歸去尋找所使用的組件   	*	@param {String} 當前文件的上下文路徑   	*	@param {String} 依賴路徑   	* @param {Array} 包含全部入口的數組   	*/   addDependencies(context, dependPath, entries) {    /** 生成絕對路徑 */     const isAbsolute = dependPath[0] === '/';    let absolutePath = '';    if (isAbsolute) {       absolutePath = path.resolve(this.appContext, dependPath.slice(1));     } else {       absolutePath = path.resolve(context, dependPath);     }    /** 生成以源代碼目錄為基準的相對路徑 */     const relativePath = path.relative(this.appContext, absolutePath);    /** 校驗該路徑是否合法以及是否在已有入口當中 */     const jsPath = replaceExt(absolutePath, '.js');    const isQualification = fs.existsSync(jsPath);    if (!isQualification) {      console.log(chalk.red(`ERROR: in "${replaceExt(relativePath, '.js')}": 當前文件缺失`));       process.exit();     }    const isExistence = entries.includes((entry) => entry === absolutePath);    if (!isExistence) {       entries.push(relativePath);     }    /** 獲取json文件內容 */     const jsonPath = replaceExt(absolutePath, '.json');    const isJsonExistence = fs.existsSync(jsonPath);    if (!isJsonExistence) {      console.log(chalk.red(`ERROR: in "${replaceExt(relativePath, '.json')}": 當前文件缺失`));       process.exit();     }    try {      const content = fs.readFileSync(jsonPath, 'utf8');      const { usingComponents = {} } = JSON.parse(content);      const components = Object.values(usingComponents);      const { length } = components;      /** 當json文件中有再引用其他組件時執行遞歸 */       if (length) {        const absoluteDir = path.dirname(absolutePath);         components.forEach((component) => {          this.addDependencies(absoluteDir, component, entries);         });       }     } catch (e) {      console.log(chalk.red(`ERROR: in "${replaceExt(relativePath, '.json')}": 當前文件內容為空或書寫不正確`));       process.exit();     }   }  /**   	* 將入口加入到webpack中   	*/   applyEntry(context, entryName, module) {    if (Array.isArray(module)) {      return new MultiEntryPlugin(context, module, entryName);     }    return new SingleEntryPlugin(context, module, entryName);   }    apply(compiler) {    /** 設置源代碼的上下文 */     const { context } = compiler.options;    this.appContext = context;      compiler.hooks.entryOption.tap('EntryExtractPlugin', () => {      /** 生成入口依賴數組 */       this.pages = this.getPages();      this.pages.forEach((page) => void this.addDependencies(context, page, this.entries));      this.entries.forEach((entry) => {        this.applyEntry(context, entry, `./${entry}`).apply(compiler);       });     });      compiler.hooks.watchRun.tap('EntryExtractPlugin', () => {      /** 校驗頁面入口是否增加 */       const pages = this.getPages();      const diffPages = difference(pages, this.pages);      const { length } = diffPages;      if (length) {        this.pages = this.pages.concat(diffPages);        const entries = [];        /** 通過新增的入口頁面建立依賴 */         diffPages.forEach((page) => void this.addDependencies(context, page, entries));        /** 去除與原有依賴的交集 */         const diffEntries = difference(entries, this.entries);         diffEntries.forEach((entry) => {          this.applyEntry(context, entry, `./${entry}`).apply(compiler);         });        this.entries = this.entries.concat(diffEntries);       }     });   } }module.exports = EntryExtractPlugin;復制代碼

        由于webpack的plugin相關知識不在我們這篇文章的討論范疇,所以我只簡單的介紹一下它是如何介入webpack的工作流程中并生成入口的。(如果有興趣想了解這些可以私信我,有時間的話可能會整理一些資料出來給大家)該插件實際做了兩件事:

        1. 通過compiler的entryOption鉤子,我們將遞歸生成的入口數組一項一項的加入entry中。
        2. 通過compiler的watchRun鉤子監聽重新編譯時是否有新的頁面加入,如果有就會以新加入的頁面生成一個依賴數組,然后再加入entry中。

        現在我們將這個插件應用到之前的webpack策略中,將上面的配置更改為:(記得安裝chalk replace-ext依賴)

        /** build/webpack.config.js */const EntryExtractPlugin = require('./entry-extract-plugin');module.exports = {   ...   entry: {    app: './app.js'   },  plugins: [     ...     new EntryExtractPlugin()   ] }復制代碼

        樣式預編譯與EsLint

        樣式預編譯和EsLint應用其實已經有許多優秀的文章了,在這里我就只貼出我們的實踐代碼:

        /** build/webpack.config.js */const MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = {   ...   module: {    rules: [       ...       {        enforce: 'pre',        test: /.js$/,        exclude: /node_modules/,        loader: 'eslint-loader',        options: {          cache: true,          fix: true,         },       },       {        test: /.less$/,        use: [           {            loader: MiniCssExtractPlugin.loader,           },           {            loader: 'css-loader',           },           {            loader: 'less-loader',           },         ],       },     ]   },  plugins: [     ...     new MiniCssExtractPlugin({ filename: '[name].wxss' })   ] }復制代碼

        我們修改完策略后就可以將wxss后綴名的文件更改為less后綴名(如果你想用其他的預編譯語言,可以自行修改loader),然后我們在js文件中加入import './index.less'語句就能看到樣式文件正常編譯生成了。樣式文件能夠正常的生成最大的功臣就是mini-css-extract-plugin工具包,它幫助我們轉換了后綴名并且生成到目標目錄中。

        環境切換

        環境變量的切換我們使用cross-env工具包來進行配置,我們在package.json文件中添加兩句腳本命令:

        "scripts": { 	"dev": "cross-env OPERATING_ENV=development webpack --config build/webpack.config.js --watch", 	"build": "cross-env OPERATING_ENV=production webpack --config build/webpack.config.js }復制代碼

        相應的我們也修改一下webpack的配置文件,將我們應用的環境也告訴webpack,這樣webpack會針對環境對代碼進行優化處理。

        /** build/webpack.config.js */const { OPERATING_ENV } = process.env;module.exports = {   ...   mode: OPERATING_ENV,  devtool: OPERATING_ENV === 'production' ? 'source-map' : 'inline-source-map'}復制代碼

        雖然我們也可以通過命令為webpack設置mode,這樣也可以在項目中通過process.env.NODE_ENV訪問環境變量,但是我還是推薦使用工具包,因為你可能會有多個環境uat test pre等等。

        針對JS優化

        小程序對包的大小有嚴格的要求,單個包的大小不能超過2M,所以我們應該對JS做進一步的優化,這有利于我們控制包的大小。我所做的優化主要針對runtime和多個入口頁面之間引用的公共部分,修改配置文件為:

        /** build/webpack.config.js */module.exports = {   ...   optimization: {    splitChunks: {      cacheGroups: {        commons: {          chunks: 'initial',          name: 'commons',          minSize: 0,          maxSize: 0,          minChunks: 2,         },       },     },    runtimeChunk: {      name: 'manifest',     },   }, }復制代碼

        webpack會將公共的部分抽離出來在dist文件夾根目錄中生成common.jsmanifest.js文件,這樣整個項目的體積就會有明顯的縮小,但是你會發現當我們運行命令是開發者工具里面項目其實是無法正常運行的,這是為什么?

        這主要是因為這種優化使小程序其他的js文件丟失了對公共部分的依賴,我們對webpack配置文件做如下修改就可以解決了:

        /** build/webpack.config.js */module.exports = {   ...   output: {     ...     globalObject: 'global'   },  plugins: [    new webpack.BannerPlugin({      banner: 'const commons = require("./commons");nconst runtime = require("./runtime");',      raw: true,      include: 'app.js',     })   ] }復制代碼

        相關學習推薦:js視頻教程

        小小解惑

        許多讀者可能會有疑惑,為什么你不直接使用已有的框架進行開發,這些能力已經有許多框架支持了。選擇框架確實是一個不錯的選擇,畢竟開箱即用為開發者帶來了許多便利。但是這個選擇是有利有弊的,我也對市面上的較流行框架做了一段時間的研究和實踐。較為早期的騰訊的wepy、美團的mpvue,后來者居上的京東的taro、Dcloud的uni-app等,這些在應用當中我認為有以下一些點不受我青睞:

        • 黑盒使我們有時很難定位問題究竟是出在自身的代碼當中還是在框架的編譯流程中(這讓我踩了不少坑)
        • 圍繞框架展開的可以使用的資源有限,例如UI的使用基本依賴于官方團隊進行配套開發,如果沒有社區也極難找到需要的資源(這一點我認為uni-app的社區做得挺不錯)
        • 與已有的一些原生的資源無法結合,這些框架基本都是基于編譯原理提供了以react或者vue為開發語言的能力,這使得原生的資源要無縫接入很難實現(假如你們公司已經積淀了一些業務組件那你會很頭疼)。
        • 最后一點,也是我擔心的最重要的一點,框架的升級速度是否能跟得上官方的迭代速度,如果滯后了已有的項目該如何處理

        以上基本是我為什么要自己探索小程序工程化的理由(其實還有一點就是求知欲,嘻嘻)

        寫在最后

        以上是我對原生小程序工程化的探索,在我所在的團隊中還應用了一些相關的樣式規范,在這篇文章中我沒有具體的說,有興趣的話可以查看我的專欄中《團隊規范之樣式規范實踐》一文。其實還有靜態資源的管理,項目的目錄的補充這些細節可以依照團隊的需要去完善補充。本文希望對有需要做這方面實踐的團隊有所幫助,如有觀點不正確或需要改進的地方,望可以評論告知我。

        贊(0)
        分享到: 更多 (0)
        網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
        主站蜘蛛池模板: 日韩精品一区二区三区中文字幕 | 国产精品婷婷午夜在线观看| 亚洲情侣偷拍精品| 国产国产成人久久精品| 99久久99久久久精品齐齐| 欧洲精品码一区二区三区免费看| 国产精品久久国产精麻豆99网站| 青草国产精品久久久久久| 老子影院午夜精品无码| 2021最新国产精品一区| 国产精品爱啪在线线免费观看| 漂亮人妻被黑人久久精品| 亚洲国产精品成人网址天堂| 国产在线观看一区精品| 91久久精品国产成人久久| 国产短视频精品一区二区三区| 久久精品中文无码资源站| 在线观看自拍少妇精品| 无码国产亚洲日韩国精品视频一区二区三区| 99热精品毛片全部国产无缓冲| 国产精品久久久久久久久| 精品三级AV无码一区| 漂亮人妻被黑人久久精品| 久久国产亚洲精品无码| 亚洲精品乱码久久久久久自慰| 亚洲精品久久久www| 日韩精品亚洲专区在线观看| 久久久久这里只有精品| 久久国产精品免费一区| 精品久久久久久无码免费| 国产精品男男视频一区二区三区| 亚洲国产精品久久久久网站| 亚洲精品视频在线| 91精品国产91久久久久久| 91不卡在线精品国产| 国产精品99精品视频网站| 国产精品无码一区二区在线观一| 粉嫩精品美女国产在线观看| 99久久国产综合精品成人影院| 99视频在线精品国自产拍亚瑟| 亚洲午夜精品一区二区|