亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

一文帶你詳細理解uni-app如何構建小程序

 更新時間:2022年11月14日 10:11:20   作者:luocheng  
uni-app是近年來一種新興的多端混合開發(fā)框架,適合開發(fā)跨平臺應用,方便多端運行,下面這篇文章主要給大家介紹了關于uni-app如何構建小程序的相關資料,需要的朋友可以參考下

前言

uni-app是一個基于Vue.js語法開發(fā)小程序的前端框架,開發(fā)者通過編寫一套代碼,可發(fā)布到iOS、Android、Web以及各種小程序平臺。今天,我們通過相關案例分析uni-app是怎樣把Vue.js構建成原生小程序的。

Vue是template、script、style三段式的SFC,uni-app是怎么把SFC拆分成小程序的ttml、ttss、js、json四段式?帶著問題,本文將從webpack、編譯器、運行時三方面帶你了解uni-app是如何構建小程序的。

一.用法

uni-app是基于vue-cli腳手架開發(fā),集成一個遠程的Vue Preset

npm install -g @vue/cli
vue create -p dcloudio/uni-preset-vue my-project

uni-app目前集成了很多不同的項目模版,可以根據不同的需要,選擇不同的模版

運行、發(fā)布uni-app,以字節(jié)小程序為例

npm run dev:mp-toutiao
npm run build:mp-toutiao

二.原理

uni-app是一個比較傳統(tǒng)的小程序框架,包括編譯器+運行時。 小程序是視圖和邏輯層分開的雙線程架構,視圖和邏輯的加載和運行互不阻塞,同時,邏輯層數(shù)據更新會驅動視圖層的更新,視圖的事件響應,會觸發(fā)邏輯層的交互。 uni-app的源碼主要包括三方面:

  • webpack。webpack是前端常用的一個模塊打包器,uni-app構建過程中,會將Vue SFC的template、script、style三段式的結構,編譯成小程序四段式結構,以字節(jié)小程序為例,會得到ttml、ttss、js、json四種文件。
  • 編譯器。uni-app的編譯器本質是把Vue 的視圖編譯成小程序的視圖,即把template語法編譯成小程序的ttml語法,之后,uni-app不會維護視圖層,視圖層的更新完全交給小程序自身維護。但是uni-app是使用Vue進行開發(fā)的,那Vue跟小程序是怎么交互的呢?這就依賴于uni-app的運行時。
  • 運行時。運行時相當于一個橋梁,打通了Vue和小程序。小程序視圖層的更新,比如事件點擊、觸摸等操作,會經過運行時的事件代理機制,然后到達Vue的事件函數(shù)。而Vue的事件函數(shù)觸發(fā)了數(shù)據更新,又會重新經過運行時,觸發(fā)setData,進一步更新小程序的視圖層。 備注:本文章閱讀的源碼是uni-app ^2.0.0-30720210122002版本。

三.webpack

1. package.json

先看package.json scripts命令:

  • 注入NODE_ENV和UNI_PLATFORM命令
  • 調用vue-cli-service命令,執(zhí)行uni-build命令
"dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch",

2. 入口

當我們在項目內部運行 vue-cli-service 命令時,它會自動解析并加載 package.json 中列出的所有 CLI 插件,Vue CLI 插件的命名遵循 vue-cli-plugin- 或者 @scope/vue-cli-plugin-的規(guī)范,這里主要的插件是@dcloudio/vue-cli-plugin-uni,相關源碼:

module.exports = (api, options) => {
  api.registerCommand('uni-build', {
    description: 'build for production',
    usage: 'vue-cli-service uni-build [options]',
    options: {
      '--watch': 'watch for changes',
      '--minimize': 'Tell webpack to minimize the bundle using the TerserPlugin.',
      '--auto-host': 'specify automator host',
      '--auto-port': 'specify automator port'
    }
  }, async (args) => {
    for (const key in defaults) {
      if (args[key] == null) {
        args[key] = defaults[key]
      }
    }

    require('./util').initAutomator(args)

    args.entry = args.entry || args._[0]

    process.env.VUE_CLI_BUILD_TARGET = args.target

    // build函數(shù)會去獲取webpack配置并執(zhí)行
    await build(args, api, options)

    delete process.env.VUE_CLI_BUILD_TARGET
  })
}

當我們執(zhí)行UNI_PLATFORM=mp-toutiao vue-cli-service uni-build時,@dcloudio/vue-cli-plugin-uni無非做了兩件事:

  • 獲取小程序的webpack配置。
  • 執(zhí)行uni-build命令時,然后執(zhí)行webpack。 所以,入口文件其實就是執(zhí)行webpack,uni-appwebpack配置主要位于@dcloudio/vue-cli-plugin-uni/lib/mp/index.js,接下來我們通過entry、output、loader、plugin來看看uni-app是怎么把Vue SFC轉換成小程序的。

3. Entry

uni-app會調用parseEntry去解析pages.json,然后放在process.UNI_ENTRY

webpackConfig () {
    parseEntry();
    return {
        entry () {
            return process.UNI_ENTRY
        }
    }
}

我們看下parseEntry主要代碼:

function parseEntry (pagesJson) {
  // 默認有一個入口
  process.UNI_ENTRY = {
    'common/main': path.resolve(process.env.UNI_INPUT_DIR, getMainEntry())
  }

  if (!pagesJson) {
    pagesJson = getPagesJson()
  }

  // 添加pages入口
  pagesJson.pages.forEach(page => {
    process.UNI_ENTRY[page.path] = getMainJsPath(page.path)
  })
}

function getPagesJson () {
  // 獲取pages.json進行解析
  return processPagesJson(getJson('pages.json', true))
}

const pagesJsonJsFileName = 'pages.js'
function processPagesJson (pagesJson) {
  const pagesJsonJsPath = path.resolve(process.env.UNI_INPUT_DIR, pagesJsonJsFileName)
  if (fs.existsSync(pagesJsonJsPath)) {
    const pagesJsonJsFn = require(pagesJsonJsPath)
    if (typeof pagesJsonJsFn === 'function') {
      pagesJson = pagesJsonJsFn(pagesJson, loader)
      if (!pagesJson) {
        console.error(`${pagesJsonJsFileName}  必須返回一個 json 對象`)
      }
    } else {
      console.error(`${pagesJsonJsFileName} 必須導出 function`)
    }
  }
  // 檢查配置是否合法
  filterPages(pagesJson.pages)
  return pagesJson
}

function getMainJsPath (page) {
  // 將main.js和page參數(shù)組合成出新的入口
  return path.resolve(process.env.UNI_INPUT_DIR, getMainEntry() + '?' + JSON.stringify({
    page: encodeURIComponent(page)
  }))
}

parseEntry的主要工作:

  • 配置默認入口main.js
  • 解析pages.json,將page作為參數(shù),和main.js組成新的入口 比如,我們的pages.json內容如下:
{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "uni-app"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  }
}

然后我們看下輸出的enrty,可以發(fā)現(xiàn)其實就是通過在main.js帶上響應參數(shù)來區(qū)分page的,這跟vue-loader區(qū)分template、script、style其實很像,后面可以通過判斷參數(shù),調用不同loader進行處理。

{
  'common/main': '/Users/src/main.js',
  'pages/index/index': '/Users/src/main.js?{"page":"pages%2Findex%2Findex"}'
}

4. Output

對于輸出比較簡單,devbuild分別打包到dist/dev/mp-toutiaodist/build/mp-toutiao

Object.assign(options, {
outputDir: process.env.UNI_OUTPUT_TMP_DIR || process.env.UNI_OUTPUT_DIR,
assetsDir
}, vueConfig)
  
webpackConfig () {
    return {
        output: {
        filename: '[name].js',
        chunkFilename: '[id].js',
    }
}

5. Alias

uni-app有兩個主要的alias配置

  • vue$是把vue替換成來uni-app的mp-vue
  • uni-pages表示pages.json文件
resolve: {
    alias: {
      vue$: getPlatformVue(vueOptions), 
      'uni-pages': path.resolve(process.env.UNI_INPUT_DIR, 'pages.json'),
    },
    modules: [
      process.env.UNI_INPUT_DIR,
      path.resolve(process.env.UNI_INPUT_DIR, 'node_modules')
    ]
},
getPlatformVue (vueOptions) {
    if (uniPluginOptions.vue) {
      return uniPluginOptions.vue
    }
    if (process.env.UNI_USING_VUE3) {
      return '@dcloudio/uni-mp-vue'
    }
    return '@dcloudio/vue-cli-plugin-uni/packages/mp-vue'
},

6. Loader

從上面我們看出entry都是main.js,只不過會帶上page的參數(shù),我們從入口開始,看下uni-app是怎么一步步處理文件的,先看下處理main.js的兩個loader:lib/mainwrap-loader

module: {
    rules: [{
      test: path.resolve(process.env.UNI_INPUT_DIR, getMainEntry()),
      use: [{
        loader: path.resolve(__dirname, '../../packages/wrap-loader'),
        options: {
          before: [
            'import \'uni-pages\';'
          ]
        }
      }, {
        loader: '@dcloudio/webpack-uni-mp-loader/lib/main'
      }]
    }]
}

a. lib/main:

我們看下核心代碼,根據resourceQuery參數(shù)進行劃分,我們主要看下有query的情況,會在這里引入Vue和pages/index/index.vue,同時調用createPage進行初始化,createPage是運行時,后面會講到。由于引入了.vue,所以之后的解析就交給了vue-loader。

module.exports = function (source, map) {
this.cacheable && this.cacheable()

  if (this.resourceQuery) {
    const params = loaderUtils.parseQuery(this.resourceQuery)
    if (params && params.page) {
      params.page = decodeURIComponent(params.page)
      // import Vue from 'vue'是為了觸發(fā) vendor 合并
      let ext = '.vue'
      return this.callback(null,
        `
import Vue from 'vue'
import Page from './${normalizePath(params.page)}${ext}'
createPage(Page)
`, map)
    }
  }    else    {......}
}

b. wrap-loader:

引入了uni-pages,從alias可知道就是import pages.json,對于pages.json,uni-app也有專門的webpack-uni-pages-loader進行處理。

module.exports = function (source, map) {
  this.cacheable()

  const opts = utils.getOptions(this) || {}
  this.callback(null, [].concat(opts.before, source, opts.after).join('').trim(), map)
}

c. webpack-uni-pages-loader:

代碼比較多,我們貼下大體的核心代碼,看看主要完成的事項

module.exports = function (content, map) {
  // 獲取mainfest.json文件
  const manifestJsonPath = path.resolve(process.env.UNI_INPUT_DIR, 'manifest.json')
  const manifestJson = parseManifestJson(fs.readFileSync(manifestJsonPath, 'utf8'))

  // 解析pages.json
  let pagesJson = parsePagesJson(content, {
    addDependency: (file) => {
      (process.UNI_PAGES_DEPS || (process.UNI_PAGES_DEPS = new Set())).add(normalizePath(file))
      this.addDependency(file)
    }
  })
  
  const jsonFiles = require('./platforms/' + process.env.UNI_PLATFORM)(pagesJson, manifestJson, isAppView)

  if (jsonFiles && jsonFiles.length) {
    jsonFiles.forEach(jsonFile => {
      if (jsonFile) {
        // 對解析到的app.json和project.config.json進行緩存
        if (jsonFile.name === 'app') {
          // updateAppJson和updateProjectJson其實就是調用updateComponentJson
          updateAppJson(jsonFile.name, renameUsingComponents(jsonFile.content))
        } else {
          updateProjectJson(jsonFile.name, jsonFile.content)
        }
      }
    })
  }

  this.callback(null, '', map)
}

function updateAppJson (name, jsonObj) {
  updateComponentJson(name, jsonObj, true, 'App')
}

function updateProjectJson (name, jsonObj) {
  updateComponentJson(name, jsonObj, false, 'Project')
}

// 更新json文件
function updateComponentJson (name, jsonObj, usingComponents = true, type = 'Component') {
  if (type === 'Component') {
    jsonObj.component = true
  }
  if (type === 'Page') {
    if (process.env.UNI_PLATFORM === 'mp-baidu') {
      jsonObj.component = true
    }
  }

  const oldJsonStr = getJsonFile(name)
  if (oldJsonStr) { // update
    if (usingComponents) { // merge usingComponents
      // 其實直接拿新的 merge 到舊的應該就行
      const oldJsonObj = JSON.parse(oldJsonStr)
      jsonObj.usingComponents = oldJsonObj.usingComponents || {}
      jsonObj.usingAutoImportComponents = oldJsonObj.usingAutoImportComponents || {}
      if (oldJsonObj.usingGlobalComponents) { // 復制 global components(針對不支持全局 usingComponents 的平臺)
        jsonObj.usingGlobalComponents = oldJsonObj.usingGlobalComponents
      }
    }
    const newJsonStr = JSON.stringify(jsonObj, null, 2)
    if (newJsonStr !== oldJsonStr) {
      updateJsonFile(name, newJsonStr)
    }
  } else { // add
    updateJsonFile(name, jsonObj)
  }
}

let jsonFileMap = new Map()
function updateJsonFile (name, jsonStr) {
  if (typeof jsonStr !== 'string') {
    jsonStr = JSON.stringify(jsonStr, null, 2)
  }
  jsonFileMap.set(name, jsonStr)
}

我們通過分步來了解webpack-uni-pages-loader的作用:

  • 獲取mainfest.jsonpages.json的內容
  • 分別調用updateAppJsonupdateProjectJson處理mainfest.jsonpage.json
  • updateAppJsonupdateProjectJson本質都是調用了updateComponentJson,updateComponentJson會更新json文件,最終調用updateJsonFile
  • updateJsonFilejson文件生成的關鍵點。首先會定義一個共享的jsonFileMap鍵值對象,然后這里并沒有直接生成相應的json文件,而是把mainfest.jsonpage.json處理成project.configapp,然后緩存在jsonFileMap中。
  • 這里為什么不直接生成?因為后續(xù)pages/index/index.vue里也會有json文件的生成,所以所有的json文件都是暫時緩存在jsonFileMap中,后續(xù)由plugin統(tǒng)一生成。 通俗的說,webpack-uni-pages-loader實現(xiàn)的功能就是json語法的轉換,還有就是緩存,語法轉換很簡單,只是對象key value的更改,我們可以直觀的對比下mainfest.jsonpage.json構建前后差異。
// 轉換前的page.json
{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "uni-app"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  }
}
// 轉換后得到的app.json
{
  "pages": [
    "pages/index/index"
  ],
  "subPackages": [],
  "window": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  },
  "usingComponents": {}
}

// 轉換前的mainfest.json
{
  "name": "",
  "appid": "",
  "description": "",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": true
}

// 轉換后得到的project.config.json
{
  "setting": {
    "urlCheck": true,
    "es6": false,
    "postcss": false,
    "minified": false,
    "newFeature": true
  },
  "appid": "體驗appId",
  "projectname": "uniapp-analysis"
}

d. vue-loader:

處理完js和json文件,接下來就到了vue文件的處理,vue-loader會把vue拆分成template、style、script。 對于style,其實就是css,會經過less-loader、sass-loader、postcss-loadercss-loader的處理,最后由mini-css-extract-plugin生成對應的.ttss文件。 對于script,uni-app主要配置了script loader進行處理,該過程主要是將index.vue中引入的組件抽離成index.json,然后也是跟app.json一樣,緩存在jsonFileMap數(shù)組中。

{
  resourceQuery: /vue&type=script/,
  use: [{
    loader: '@dcloudio/webpack-uni-mp-loader/lib/script'
  }]
}

對于template,這是比較核心的模塊,uni-app更改了vue-loader的compiler,將vue-template-compiler替換成了uni-template-compiler,uni-template-compiler是用來把vue語法轉換為小程序語法的,這里我們可以先記著,后面會講到是如何編譯的。這里我們關注的處理template的loader lib/template 。

{
  resourceQuery: /vue&type=template/,
  use: [{
    loader: '@dcloudio/webpack-uni-mp-loader/lib/template'
  }, {
    loader: '@dcloudio/vue-cli-plugin-uni/packages/webpack-uni-app-loader/page-meta'
  }]
}

loader lib/template首先會去獲取vueLoaderOptions,然后添加新的options,小程序這里有一個關鍵是emitFile,因為vue-loader本身是沒有往compiler注入emitFile的,所以compiler編譯出來的語法要生成ttml需要有emitFile。

module.exports = function (content, map) {
  this.cacheable && this.cacheable()
  const vueLoaderOptions = this.loaders.find(loader => loader.ident === 'vue-loader-options')
  Object.assign(vueLoaderOptions.options.compilerOptions, {
      mp: {
        platform: process.env.UNI_PLATFORM
      },
      filterModules,
      filterTagName,
      resourcePath,
      emitFile: this.emitFile,
      wxComponents,
      getJsonFile,
      getShadowTemplate,
      updateSpecialMethods,
      globalUsingComponents,
      updateGenericComponents,
      updateComponentGenerics,
      updateUsingGlobalComponents
  })
}

7. plugin

uni-app主要的plugin是createUniMPPlugin,該過程對應了我們loader處理json時生成的jsonFileMap對象,本質就是把jsonFileMap里的json生成真實的文件。

class WebpackUniMPPlugin {
  apply (compiler) {
    if (!process.env.UNI_USING_NATIVE && !process.env.UNI_USING_V3_NATIVE) {
      compiler.hooks.emit.tapPromise('webpack-uni-mp-emit', compilation => {
        return new Promise((resolve, reject) => {
          // 生成.json
          generateJson(compilation)
          // 生成app.json、project.config.json
          generateApp(compilation)
            .forEach(({
              file,
              source
            }) => emitFile(file, source, compilation))

          resolve()
        })
      })
    }

相關的全局配置變量

plugins: [
    new webpack.ProvidePlugin({
        uni: [
            '/Users/luojincheng/source code/uniapp-analysis/node_modules/@dcloudio/uni-mp-toutiao/dist/index.js',
            'default'
          ],
        createPage: [
            '/Users/luojincheng/source code/uniapp-analysis/node_modules/@dcloudio/uni-mp-toutiao/dist/index.js',
            'createPage'
          ]
    })
]

四. 編譯器知一二

編譯器的原理其實就是通過ast的語法分析,把vue的template語法轉換為小程序的ttml語法。但這樣說其實很抽象,具體是怎么通過ast語法來轉換的?接下來,我們通過構建簡單版的template=>ttml的編譯器,實現(xiàn)div=>view的標簽轉換,來了解uni-app的編譯流程。

<div style="height: 100px;"><text>hello world!</text></div>

上面這個template經過uni-app編譯后會變成下面的代碼,看這里只是div => view的替換,但其實中間是走了很多流程的。

<view style="height: 100px;"><text>hello world!</text></view>

1. vue-template-compiler

首先,template會經過vue的編譯器,得到渲染函數(shù)render。

const {compile} = require('vue-template-compiler');
const {render} = compile(state.vueTemplate);
// 生成的render:
// with(this){return _c('div',{staticStyle:{"height":"100px"}},[_c('text',[_v("hello world!")])])}

2. @babel/parser

這一步是利用parser將render函數(shù)轉化為ast。ast是Abstract syntax tree的縮寫,即抽象語法樹。

const parser = require('@babel/parser');
const ast = parser.parse(render);

這里我們過濾掉了一些start、end、loc、errors等會影響我們閱讀的字段(完整ast可以通過 astexplorer.net網站查看),看看轉譯后的ast對象,該json對象我們重點關注program.body[0].expression。 1.type的類型在這里有四種:

  • CallExpression(調用表達式):_c()
  • StringLiteral(字符串字面量):'div'
  • ObjectExpression(對象表達式):'{}'
  • ArrayExpression(數(shù)組表達式):[_v("hello world!")] 2.callee.name是調用表達式的名稱:這里有_c、_v兩種 3.arguments.*.value是參數(shù)的值:這里有div、text、hello world! 我們把ast對象和render函數(shù)對比,不難發(fā)現(xiàn)這兩個其實是一一對應可逆的關系。
{
  "type": "File",
  "program": {
    "type": "Program",
    },
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "ExpressionStatement",
        "expression": {
          "callee": {
            "type": "Identifier",
            "name": "_c"
          },
          "arguments": [
            {
              "type": "StringLiteral",
              "value": "div"
            },
            {
              "type": "ObjectExpression",
              "properties": [
                {
                  "type": "ObjectProperty",
                  "method": false,
                  "key": {
                    "type": "Identifier",
                    "name": "staticStyle"
                  },
                  "computed": false,
                  "shorthand": false,
                  "value": {
                    "type": "ObjectExpression",
                    "properties": [
                      {
                        "type": "ObjectProperty",
                        "method": false,
                        "key": {
                          "type": "StringLiteral",
                          "value": "height"
                        },
                        "computed": false,
                        "shorthand": false,
                        "value": {
                          "type": "StringLiteral",
                          "value": "100px"
                        }
                      }
                    ]
                  }
                }
              ]
            },
            {
              "type": "ArrayExpression",
              "elements": [
                {
                  "type": "CallExpression",
                  "callee": {
                    "name": "_c"
                  },
                  "arguments": [
                    {
                      "type": "StringLiteral",
                      "value": "text"
                    },
                    {
                      "type": "ArrayExpression",
                      "elements": [
                        {
                          "type": "CallExpression",
                          "callee": {
                            "type": "Identifier",
                            "name": "_v"
                          },
                          "arguments": [
                            {
                              "type": "CallExpression",
                              "callee": {
                                "type": "Identifier",
                                "name": "_s"
                              },
                              "arguments": [
                                {
                                  "type": "Identifier",
                                  "name": "hello"
                                }
                              ]
                            }
                          ]
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        }
      }
    ],
    "directives": []
  },
  "comments": []
}

3. @babel/traverse和@babel/types

這一步主要是利用traverse對生成的ast對象進行遍歷,然后利用types判斷和修改ast的語法。 traverse(ast, visitor)主要有兩個參數(shù):

  • parser解析出來的ast
  • visitor:visitor是一個由各種type或者是enter和exit組成的對象。這里我們指定了CallExpression類型,遍歷ast時遇到CallExpression類型會執(zhí)行該函數(shù),把對應的div、img轉換為view、image。 其它類型可看文檔:babeljs.io/docs/en/bab…
const t = require('@babel/types')
const babelTraverse = require('@babel/traverse').default

const tagMap = {
  'div': 'view',
  'img': 'image',
  'p': 'text'
};

const visitor = {
  CallExpression (path) {
    const callee = path.node.callee;
    const methodName = callee.name
    switch (methodName) {
      case '_c': {
        const tagNode = path.node.arguments[0];
        if (t.isStringLiteral(tagNode)) {
          const tagName = tagMap[tagNode.value];
          tagNode.value = tagName;
        }
      }
    }
  }
};

traverse(ast, visitor);

4. Generate vnode

uni-app生成小程序的ttml需要先把修改后的ast生成類似vNode的對象,然后再遍歷vNode生成ttml。

const traverse = require('@babel/traverse').default;

traverse(ast, {
  WithStatement(path) {
    state.vNode = traverseExpr(path.node.body.body[0].argument);
  },
});

// 不同的element走不同的創(chuàng)建函數(shù)
function traverseExpr(exprNode) {
  if (t.isCallExpression(exprNode)) {
    const traverses = {
      _c: traverseCreateElement,
      _v: traverseCreateTextVNode,
    };
    return traverses[exprNode.callee.name](exprNode);
  } else if (t.isArrayExpression(exprNode)) {
    return exprNode.elements.reduce((nodes, exprNodeItem) => {
      return nodes.concat(traverseExpr(exprNodeItem, state));
    }, []);
  }
}

// 轉換style屬性
function traverseDataNode(dataNode) {
  const ret = {};
  dataNode.properties.forEach((property) => {
    switch (property.key.name) {
      case 'staticStyle':
        ret.style = property.value.properties.reduce((pre, {key, value}) => {
          return (pre += `${key.value}: ${value.value};`);
        }, '');
        break;
    }
  });
  return ret;
}

// 創(chuàng)建Text文本節(jié)點
function traverseCreateTextVNode(callExprNode) {
  const arg = callExprNode.arguments[0];
  if (t.isStringLiteral(arg)) {
    return arg.value;
  }
}

// 創(chuàng)建element節(jié)點
function traverseCreateElement(callExprNode) {
  const args = callExprNode.arguments;
  const tagNode = args[0];

  const node = {
    type: tagNode.value,
    attr: {},
    children: [],
  };

  if (args.length < 2) {
    return node;
  }

  const dataNodeOrChildNodes = args[1];
  if (t.isObjectExpression(dataNodeOrChildNodes)) {
    Object.assign(node.attr, traverseDataNode(dataNodeOrChildNodes));
  } else {
    node.children = traverseExpr(dataNodeOrChildNodes);
  }

  if (args.length < 3) {
    return node;
  }
  const childNodes = args[2];
  if (node.children && node.children.length) {
    node.children = node.children.concat(traverseExpr(childNodes));
  } else {
    node.children = traverseExpr(childNodes, state);
  }

  return node;
}

這里之所以沒有使用@babel/generator,是因為使用generator生成的還是render函數(shù),雖然語法已經修改了,但要根據render是沒辦法直接生成小程序的ttml,還是得轉換成vNode。 最好,我們看下生成的VNode對象。

{
    "type": "view",
    "attr": {
        "style": "height: 100px;"
    },
    "children": [{
        "type": "text",
        "attr": {},
        "children": ["hello world!"]
    }]
}

5. Generate code

遍歷VNode,遞歸生成小程序代碼

function generate(vNode) {
  if (!vNode) {
    return '';
  }

  if (typeof vNode === 'string') {
    return vNode;
  }

  const names = Object.keys(vNode.attr);
  const props = names.length
    ? ' ' +
      names
        .map((name) => {
          const value = vNode.attr[name];
          return `${name}="${value}"`;
        })
        .join(' ')
    : '';
  const children = vNode.children
    .map((child) => {
      return generate(child);
    })
    .join('');

  return `<${vNode.type}${props}>${children}</${vNode.type}>`;
}

6. 總體流程:

這里列出了uni-template-compiler大致轉換的流程和關鍵代碼,uni-template-compiler的ast語法轉換工作都是在traverse這個過程完成的。

const {compile} = require('vue-template-compiler');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

const state = {
  vueTemplate: '<div style="height: 100px;"><text>hello world!</text></div>',
  mpTemplate: '',
  vNode: '',
};
const tagMap = {
  div: 'view',
};

// 1.vue template => vue render
const {render} = compile(state.vueTemplate);
// 2.vue render => code ast
const ast = parser.parse(`function render(){${render}}`);
// 3.map code ast, modify syntax
traverse(ast, getVisitor());
// 4.code ast => mp vNode
traverse(ast, {
  WithStatement(path) {
    state.vNode = traverseExpr(path.node.body.body[0].argument);
  },
});
// 5.mp vNode => ttml
state.mpTemplate = generate(state.vNode);

console.log('vue template:', state.vueTemplate);
console.log('mp  template:', state.mpTemplate);

五.運行時的原理

uni-app提供了一個運行時uni-app runtime,打包到最終運行的小程序發(fā)行代碼中,該運行時實現(xiàn)了Vue.js 和小程序兩系統(tǒng)之間的數(shù)據、事件同步。

1.事件代理

我們以一個數(shù)字增加為例,看看uni-app是怎樣把小程序的數(shù)據、事件跟vue整合起來的。

<template>
  <div @click="add(); subtract(2)" @touchstart="mixin($event)">{{ num }}</div>
</template>

<script>
export default {
  data() {
    return {
      num1: 0,
      num2: 0,
    }
  },
  methods: {
    add () {
      this.num1++;
    },
    subtract (num) {
      console.log(num)
    },
    mixin (e) {
      console.log(e)
    }
  }
}
</script>

a. 編譯后的ttml,這里編譯出來data-event-opts、bindtap跟前面的編譯器div => view的原理是差不多,也是在traverse做的ast轉換,我們直接看編譯后生成的ttml:

<view 
    data-event-opts="{{
        [
            ['tap',[['add'],['subtract',[2]]]],
            ['touchstart',[['mixin',['$event']]]]
        ]
    }}"
    bindtap="__e" bindtouchstart="__e"
    class="_div">
    {{num}}
</view>

這里我們先解析一下data-event-opts數(shù)組: data-event-opts是一個二維數(shù)組,每個子數(shù)組代表一個事件類型。子數(shù)組有兩個值,第一個表示事件類型名稱,第二個表示觸發(fā)事件函數(shù)的個數(shù)。事件函數(shù)又是一個數(shù)組,第一個值表述事件函數(shù)名稱,第二個是參數(shù)個數(shù)。 ['tap',[['add'],['subtract',[2]]]]表示事件類型為tap,觸發(fā)函數(shù)有兩個,一個為add函數(shù)且無參數(shù),一個為subtract且參數(shù)為2。 ['touchstart',[['mixin',['$event']]]]表示事件類型為touchstart,觸發(fā)函數(shù)有一個為mixin,參數(shù)為$event對象。

b. 編譯后的js的代碼:

import Vue from 'vue'
import Page from './index/index.vue'
createPage(Page)

這里其實就是后調用uni-mp-toutiao里的createPage對vue的script部分進行了初始化。 createPage返回小程序的Component構造器,之后是一層層的調用parsePage、parseBasePage、parseComponent、parseBaseComponent,parseBaseComponent最后返回一個Component構造器

function createPage (vuePageOptions) {
  {
    return Component(parsePage(vuePageOptions))
  }
}

function parsePage (vuePageOptions) {
  const pageOptions = parseBasePage(vuePageOptions, {
    isPage,
    initRelation
  });
  return pageOptions
}

function parseBasePage (vuePageOptions, {
  isPage,
  initRelation
}) {
  const pageOptions = parseComponent(vuePageOptions);

  return pageOptions
}

function parseComponent (vueOptions) {
  const [componentOptions, VueComponent] = parseBaseComponent(vueOptions);

  return componentOptions
}

我們直接對比轉換前后的vue和mp參數(shù)差異,本身vue的語法和mp Component的語法就很像。這里,uni-app會把vue的data屬性和methods方法copy到mp的data,而且mp的methods主要有__e方法。

回到編譯器生成ttml代碼,發(fā)現(xiàn)所有的事件都會調用__e事件,而__e對應的就是handleEvent事件,我們看下handleEvent

  • 拿到點擊元素上的data-event-opts屬性:[['tap',[['add'],['subtract',[2]]]],['touchstart',[['mixin',['$event']]]]]
  • 根據點擊類型獲取相應數(shù)組,比如bindTap就取['tap',[['add'],['subtract',[2]]]],bindtouchstart就取['touchstart',[['mixin',['$event']]]]
  • 依次調用相應事件類型的函數(shù),并傳入參數(shù),比如tap調用this.add();this.subtract(2)
function handleEvent (event) {
  event = wrapper$1(event);

  // [['tap',[['handle',[1,2,a]],['handle1',[1,2,a]]]]]
  const dataset = (event.currentTarget || event.target).dataset;
  const eventOpts = dataset.eventOpts || dataset['event-opts']; // 支付寶 web-view 組件 dataset 非駝峰

  // [['handle',[1,2,a]],['handle1',[1,2,a]]]
  const eventType = event.type;

  const ret = [];

  eventOpts.forEach(eventOpt => {
    let type = eventOpt[0];
    const eventsArray = eventOpt[1];

    if (eventsArray && isMatchEventType(eventType, type)) {
      eventsArray.forEach(eventArray => {
        const methodName = eventArray[0];
        if (methodName) {
          let handlerCtx = this.$vm;
          if (handlerCtx.$options.generic) { // mp-weixin,mp-toutiao 抽象節(jié)點模擬 scoped slots
            handlerCtx = getContextVm(handlerCtx) || handlerCtx;
          }
          if (methodName === '$emit') {
            handlerCtx.$emit.apply(handlerCtx,
              processEventArgs(
                this.$vm,
                event,
                eventArray[1],
                eventArray[2],
                isCustom,
                methodName
              ));
            return
          }
          const handler = handlerCtx[methodName];
          const params = processEventArgs(
            this.$vm,
            event,
            eventArray[1],
            eventArray[2],
            isCustom,
            methodName
          );
          ret.push(handler.apply(handlerCtx, (Array.isArray(params) ? params : []).concat([, , , , , , , , , , event])));
        }
      });
    }
  });
}

2. 數(shù)據同步機制

小程序視圖層事件響應,會觸發(fā)小程序邏輯事件,邏輯層會調用vue相應的事件,觸發(fā)數(shù)據更新。那Vue數(shù)據更新之后,又是怎樣觸發(fā)小程序視圖層更新的呢?

小程序數(shù)據更新必須要調用小程序的setData函數(shù),而Vue數(shù)據更新的時候會觸發(fā)Vue.prototype._update方法,所以,只要在_update里調用setData函數(shù)就可以了。 uni-app在Vue里新增了patch函數(shù),該函數(shù)會在_update時被調用。

// install platform patch function
Vue.prototype.__patch__ = patch;

var patch = function(oldVnode, vnode) {
  var this$1 = this;

  if (vnode === null) { //destroy
    return
  }
  if (this.mpType === 'page' || this.mpType === 'component') {
    var mpInstance = this.$scope;
    var data = Object.create(null);
    try {
      data = cloneWithData(this);
    } catch (err) {
      console.error(err);
    }
    data.__webviewId__ = mpInstance.data.__webviewId__;
    var mpData = Object.create(null);
    Object.keys(data).forEach(function (key) { //僅同步 data 中有的數(shù)據
      mpData[key] = mpInstance.data[key];
    });
    var diffData = this.$shouldDiffData === false ? data : diff(data, mpData);
    if (Object.keys(diffData).length) {
      if (process.env.VUE_APP_DEBUG) {
        console.log('[' + (+new Date) + '][' + (mpInstance.is || mpInstance.route) + '][' + this._uid +
          ']差量更新',
          JSON.stringify(diffData));
      }
      this.__next_tick_pending = true
      mpInstance.setData(diffData, function () {
        this$1.__next_tick_pending = false;
        flushCallbacks$1(this$1);
      });
    } else {
      flushCallbacks$1(this);
    }
  }
};

源代碼比較簡單,就是比對更新前后的數(shù)據,然后獲得diffData,最后批量調用setData更新數(shù)據。

3. diff算法

小程序數(shù)據更新有三種情況

  • 類型改變
  • 減量更新
  • 增量更新
page({
    data:{
        list:['item1','item2','item3','item4']
    },
    change(){
        // 1.類型改變
        this.setData({
            list: 'list'
        })
    },
    cut(){
        // 2.減量更新
        let newData = ['item5', 'item6'];
        this.setData({
            list: newData
        })
    },
    add(){
        // 3.增量更新
        let newData = ['item5','item6','item7','item8'];
        this.data.list.push(...newData); //列表項新增記錄
        this.setData({
            list:this.data.list
        })
    }
})

對于類型替換或者減量更新,我們只要直接替換數(shù)據即可,但對于增量更新,如果進行直接數(shù)據替換,會有一定的性能問題,比如上面的例子,將item1~item4更新為了item1~item8,這個過程我們需要8個數(shù)據全部傳遞過去,但是實踐上只更新了item5~item8。在這種情況下,為了優(yōu)化性能,我們必須要采用如下寫法,手動進行增量更新:

this.setData({
    list[4]: 'item5',
    list[5]: 'item6',
    list[6]: 'item7',
    list[7]: 'item8',
})

這種寫法的開發(fā)體驗極差,而且不便于維護,所以uni-app借鑒了westore JSON Diff的原理,在setData時進行了差量更新,下面,讓我們通過源碼,來了解diff的原理吧。

function setResult(result, k, v) {
    result[k] = v;
}

function _diff(current, pre, path, result) {
    if (current === pre) {
       // 更新前后無改變
      return;
    }
    var rootCurrentType = type(current);
    var rootPreType = type(pre);
    if (rootCurrentType == OBJECTTYPE) {
      // 1.對象類型
      if (rootPreType != OBJECTTYPE || Object.keys(current).length < Object.keys(pre).length) {
        // 1.1數(shù)據類型不一致或者減量更新,直接替換
        setResult(result, path, current);
      } else {
        var loop = function (key) {
          var currentValue = current[key];
          var preValue = pre[key];
          var currentType = type(currentValue);
          var preType = type(preValue);
          if (currentType != ARRAYTYPE && currentType != OBJECTTYPE) {
            // 1.2.1 處理基礎類型
            if (currentValue != pre[key]) {
              setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
            }
          } else if (currentType == ARRAYTYPE) {
            // 1.2.2 處理數(shù)組類型
            if (preType != ARRAYTYPE) {
              // 類型不一致
              setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
            } else {
              if (currentValue.length < preValue.length) {
                // 減量更新
                setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
              } else {
                // 增量更新則遞歸
                currentValue.forEach(function (item, index) {
                  _diff(item, preValue[index], (path == '' ? '' : path + '.') + key + '[' + index + ']', result);
                });
              }
            }
          } else if (currentType == OBJECTTYPE) {
            // 1.2.3 處理對象類型
            if (preType != OBJECTTYPE || Object.keys(currentValue).length < Object.keys(preValue).length) {
              // 類型不一致/減量更新
              setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
            } else {
              // 增量更新則遞歸
              for (var subKey in currentValue) {
                _diff(
                  currentValue[subKey],
                  preValue[subKey],
                  (path == '' ? '' : path + '.') + key + '.' + subKey,
                  result
                );
              }
            }
          }
        };
        // 1.2遍歷對象/數(shù)據類型
        for (var key in current) loop(key);
      }
    } else if (rootCurrentType == ARRAYTYPE) {
      // 2.數(shù)組類型
      if (rootPreType != ARRAYTYPE) {
        // 類型不一致
        setResult(result, path, current);
      } else {
        if (current.length < pre.length) {
          // 減量更新
          setResult(result, path, current);
        } else {
          // 增量更新則遞歸
          current.forEach(function (item, index) {
            _diff(item, pre[index], path + '[' + index + ']', result);
          });
        }
      }
    } else {
      // 3.基本類型
      setResult(result, path, current);
    }
  },
  • 當數(shù)據發(fā)生改變時,uni-app會將新舊數(shù)據進行比對,然后獲得差量更新的數(shù)據,調用setData更新。
  • 通過cur === pre進行判斷,相同則直接返回。
  • 通過type(cur) === OBJECTTYPE進行對象判斷:
    • pre不是OBJECTTYPE或者cur長度少于pre,則是類型改變或者減量更新,調用setResult直接添加新數(shù)據。
    • 否則執(zhí)行增量更新邏輯:
      • 遍歷cur,對每個key批量調用loop函數(shù)進行處理。
      • currentType不是ARRAYTYPE或者OBJECTTYPE,則是類型改變,調用setResult直接添加新數(shù)據。
      • currentTypeARRAYTYPE
        • preType不是ARRAYTYPE,或者currentValue長度少于preValue,則是類型改變或者減量更新,調用setResult直接添加新數(shù)據。
        • 否則執(zhí)行增量更新邏輯,遍歷currentValue,執(zhí)行_diff進行遞歸。
      • currentTypeOBJECTTYPE:
        • preType不是OBJECTTYPE或者currentValue長度少于preValue,則是類型改變或者減量更新,調用setResult直接添加新數(shù)據。
        • 否則執(zhí)行增量更新邏輯,遍歷currentValue,執(zhí)行_diff進行遞歸。
  • 通過type(cur) === ARRAYTYPE進行數(shù)組判斷:
    • preType不是OBJECTTYPE或者currentValue長度少于preValue,則是類型改變或者減量更新,調用setResult直接添加新數(shù)據。
    • 否則執(zhí)行增量更新邏輯,遍歷currentValue,執(zhí)行_diff進行遞歸。
  • 若以上三個判斷居不成立,則判斷是基礎類型,調用setResult添加新數(shù)據。 小結:_diff的過程,主要進行對象、數(shù)組和基礎類型的判斷。只有基本類型、類型改變、減量更新會進行setResult,否則進行遍歷遞歸_diff。

六.對比

uni-app是編譯型的框架,雖然目前市面上運行型的框架層出不窮,比如Rax 運行時/Remax/Taro Next。對比這些,uni-app這類編譯型的框架的劣勢在于語法支持,運行型的框架幾乎沒有語法限制,而uni-app因為ast的復雜度和可轉換性,導致部分語法無法支持。但是運行時也有缺點,運行型用的是小程序的模版語法template,而uni-app采用Component構造器,使用Component的好處就是原生框架可以知道頁面的大體結構,而template模版渲染無法做到,同時,運行型框架數(shù)據傳輸量大,需要將數(shù)據轉換成VNode傳遞個視圖層,這也是運行型性能損耗的原因。

七.總結

七.參考資料

uni-app官網

前端搞跨端跨棧|保哥-如何打磨 uni-app 跨端框架的高性能和易用性 · 語雀

前端搞跨端跨棧|JJ-如何借助 Taro Next 橫穿跨端業(yè)務線 · 語雀

在 2020 年,談小程序框架該如何選擇

總結

到此這篇關于uni-app如何構建小程序的文章就介紹到這了,更多相關uni-app構建小程序內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 詳解vue.js根據不同環(huán)境(正式、測試)打包到不同目錄

    詳解vue.js根據不同環(huán)境(正式、測試)打包到不同目錄

    這篇文章主要介紹了詳解vue.js根據不同環(huán)境(正式、測試)打包到不同目錄,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-07-07
  • JavaScript 對象成員的可見性說明

    JavaScript 對象成員的可見性說明

    與java等基于類的面向對象語言的private、protected、public等關鍵字的用途類似,基于對象的JavaScript語言,在對象構造上也存在類似的成員可見性問題。
    2009-10-10
  • 微信小程序實現(xiàn)藍牙設備搜索及連接功能示例詳解

    微信小程序實現(xiàn)藍牙設備搜索及連接功能示例詳解

    這篇文章主要介紹了微信小程序實現(xiàn)藍牙設備搜索及連接功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-06-06
  • Js中Symbol的靜態(tài)屬性及用途詳解

    Js中Symbol的靜態(tài)屬性及用途詳解

    JavaScript 語言在 ES6 規(guī)范中引入了 Symbol 類型,它是一種原始數(shù)據類型,用于創(chuàng)建唯一的標識符,本文將介紹 Symbol 類型的所有靜態(tài)屬性,并舉例說明它們的用途和使用場景,希望對大家有所幫助
    2023-12-12
  • 詳解JavaScript的垃圾回收機制

    詳解JavaScript的垃圾回收機制

    這篇文章主要為大家介紹了JavaScript的垃圾回收機制,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2021-11-11
  • p5.js臨摹旋轉愛心

    p5.js臨摹旋轉愛心

    這篇文章主要為大家詳細介紹了p5.js臨摹旋轉愛心,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-10-10
  • js生成隨機數(shù)的過程解析

    js生成隨機數(shù)的過程解析

    這篇文章主要介紹了js生成隨機數(shù)的過程,如何生成[n,m]的隨機整數(shù),感興趣的小伙伴們可以參考一下
    2015-11-11
  • 使用Promise封裝小程序wx.request的實現(xiàn)方法

    使用Promise封裝小程序wx.request的實現(xiàn)方法

    這篇文章主要介紹了使用Promise封裝小程序wx.request的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-11-11
  • 微信小程序Echarts覆蓋正常組件問題解決

    微信小程序Echarts覆蓋正常組件問題解決

    這篇文章主要介紹了微信小程序Echarts覆蓋正常組件問題解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2019-07-07
  • 微信小程序中實現(xiàn)車牌輸入功能

    微信小程序中實現(xiàn)車牌輸入功能

    我們都知道車牌是有一定規(guī)律的,本文實現(xiàn)了微信小程序中實現(xiàn)車牌輸入功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-05-05

最新評論