欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你詳細(xì)理解uni-app如何構(gòu)建小程序

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

前言

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

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

一.用法

uni-app是基于vue-cli腳手架開(kāi)發(fā),集成一個(gè)遠(yuǎn)程的Vue Preset

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

uni-app目前集成了很多不同的項(xiàng)目模版,可以根據(jù)不同的需要,選擇不同的模版

運(yùn)行、發(fā)布uni-app,以字節(jié)小程序?yàn)槔?/p>

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

二.原理

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

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

三.webpack

1. package.json

先看package.json scripts命令:

  • 注入NODE_ENV和UNI_PLATFORM命令
  • 調(diào)用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. 入口

當(dāng)我們?cè)陧?xiàng)目?jī)?nèi)部運(yùn)行 vue-cli-service 命令時(shí),它會(huì)自動(dòng)解析并加載 package.json 中列出的所有 CLI 插件,Vue CLI 插件的命名遵循 vue-cli-plugin- 或者 @scope/vue-cli-plugin-的規(guī)范,這里主要的插件是@dcloudio/vue-cli-plugin-uni,相關(guān)源碼:

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ù)會(huì)去獲取webpack配置并執(zhí)行
    await build(args, api, options)

    delete process.env.VUE_CLI_BUILD_TARGET
  })
}

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

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

3. Entry

uni-app會(huì)調(diào)用parseEntry去解析pages.json,然后放在process.UNI_ENTRY

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

我們看下parseEntry主要代碼:

function parseEntry (pagesJson) {
  // 默認(rèn)有一個(gè)入口
  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進(jìn)行解析
  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}  必須返回一個(gè) json 對(duì)象`)
      }
    } else {
      console.error(`${pagesJsonJsFileName} 必須導(dǎo)出 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的主要工作:

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

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

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

4. Output

對(duì)于輸出比較簡(jiǎn)單,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有兩個(gè)主要的alias配置

  • vue$是把vue替換成來(lái)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,只不過(guò)會(huì)帶上page的參數(shù),我們從入口開(kāi)始,看下uni-app是怎么一步步處理文件的,先看下處理main.js的兩個(gè)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:

我們看下核心代碼,根據(jù)resourceQuery參數(shù)進(jìn)行劃分,我們主要看下有query的情況,會(huì)在這里引入Vue和pages/index/index.vue,同時(shí)調(diào)用createPage進(jìn)行初始化,createPage是運(yùn)行時(shí),后面會(huì)講到。由于引入了.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,對(duì)于pages.json,uni-app也有專門(mén)的webpack-uni-pages-loader進(jìn)行處理。

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:

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

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) {
        // 對(duì)解析到的app.json和project.config.json進(jìn)行緩存
        if (jsonFile.name === 'app') {
          // updateAppJson和updateProjectJson其實(shí)就是調(diào)用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
      // 其實(shí)直接拿新的 merge 到舊的應(yīng)該就行
      const oldJsonObj = JSON.parse(oldJsonStr)
      jsonObj.usingComponents = oldJsonObj.usingComponents || {}
      jsonObj.usingAutoImportComponents = oldJsonObj.usingAutoImportComponents || {}
      if (oldJsonObj.usingGlobalComponents) { // 復(fù)制 global components(針對(duì)不支持全局 usingComponents 的平臺(tái))
        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)
}

我們通過(guò)分步來(lái)了解webpack-uni-pages-loader的作用:

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

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

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

d. vue-loader:

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

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

對(duì)于template,這是比較核心的模塊,uni-app更改了vue-loader的compiler,將vue-template-compiler替換成了uni-template-compiler,uni-template-compiler是用來(lái)把vue語(yǔ)法轉(zhuǎn)換為小程序語(yǔ)法的,這里我們可以先記著,后面會(huì)講到是如何編譯的。這里我們關(guān)注的處理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首先會(huì)去獲取vueLoaderOptions,然后添加新的options,小程序這里有一個(gè)關(guān)鍵是emitFile,因?yàn)関ue-loader本身是沒(méi)有往compiler注入emitFile的,所以compiler編譯出來(lái)的語(yǔ)法要生成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,該過(guò)程對(duì)應(yīng)了我們loader處理json時(shí)生成的jsonFileMap對(duì)象,本質(zhì)就是把jsonFileMap里的json生成真實(shí)的文件。

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()
        })
      })
    }

相關(guān)的全局配置變量

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'
          ]
    })
]

四. 編譯器知一二

編譯器的原理其實(shí)就是通過(guò)ast的語(yǔ)法分析,把vue的template語(yǔ)法轉(zhuǎn)換為小程序的ttml語(yǔ)法。但這樣說(shuō)其實(shí)很抽象,具體是怎么通過(guò)ast語(yǔ)法來(lái)轉(zhuǎn)換的?接下來(lái),我們通過(guò)構(gòu)建簡(jiǎn)單版的template=>ttml的編譯器,實(shí)現(xiàn)div=>view的標(biāo)簽轉(zhuǎn)換,來(lái)了解uni-app的編譯流程。

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

上面這個(gè)template經(jīng)過(guò)uni-app編譯后會(huì)變成下面的代碼,看這里只是div => view的替換,但其實(shí)中間是走了很多流程的。

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

1. vue-template-compiler

首先,template會(huì)經(jīng)過(guò)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ù)轉(zhuǎn)化為ast。ast是Abstract syntax tree的縮寫(xiě),即抽象語(yǔ)法樹(shù)。

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

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

  • CallExpression(調(diào)用表達(dá)式):_c()
  • StringLiteral(字符串字面量):'div'
  • ObjectExpression(對(duì)象表達(dá)式):'{}'
  • ArrayExpression(數(shù)組表達(dá)式):[_v("hello world!")] 2.callee.name是調(diào)用表達(dá)式的名稱:這里有_c、_v兩種 3.arguments.*.value是參數(shù)的值:這里有div、text、hello world! 我們把a(bǔ)st對(duì)象和render函數(shù)對(duì)比,不難發(fā)現(xiàn)這兩個(gè)其實(shí)是一一對(duì)應(yīng)可逆的關(guā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對(duì)生成的ast對(duì)象進(jìn)行遍歷,然后利用types判斷和修改ast的語(yǔ)法。 traverse(ast, visitor)主要有兩個(gè)參數(shù):

  • parser解析出來(lái)的ast
  • visitor:visitor是一個(gè)由各種type或者是enter和exit組成的對(duì)象。這里我們指定了CallExpression類型,遍歷ast時(shí)遇到CallExpression類型會(huì)執(zhí)行該函數(shù),把對(duì)應(yīng)的div、img轉(zhuǎn)換為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的對(duì)象,然后再遍歷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));
    }, []);
  }
}

// 轉(zhuǎn)換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é)點(diǎn)
function traverseCreateTextVNode(callExprNode) {
  const arg = callExprNode.arguments[0];
  if (t.isStringLiteral(arg)) {
    return arg.value;
  }
}

// 創(chuàng)建element節(jié)點(diǎn)
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;
}

這里之所以沒(méi)有使用@babel/generator,是因?yàn)槭褂胓enerator生成的還是render函數(shù),雖然語(yǔ)法已經(jīng)修改了,但要根據(jù)render是沒(méi)辦法直接生成小程序的ttml,還是得轉(zhuǎn)換成vNode。 最好,我們看下生成的VNode對(duì)象。

{
    "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大致轉(zhuǎn)換的流程和關(guān)鍵代碼,uni-template-compiler的ast語(yǔ)法轉(zhuǎn)換工作都是在traverse這個(gè)過(guò)程完成的。

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);

五.運(yùn)行時(shí)的原理

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

1.事件代理

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

<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,這里編譯出來(lái)data-event-opts、bindtap跟前面的編譯器div => view的原理是差不多,也是在traverse做的ast轉(zhuǎn)換,我們直接看編譯后生成的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是一個(gè)二維數(shù)組,每個(gè)子數(shù)組代表一個(gè)事件類型。子數(shù)組有兩個(gè)值,第一個(gè)表示事件類型名稱,第二個(gè)表示觸發(fā)事件函數(shù)的個(gè)數(shù)。事件函數(shù)又是一個(gè)數(shù)組,第一個(gè)值表述事件函數(shù)名稱,第二個(gè)是參數(shù)個(gè)數(shù)。 ['tap',[['add'],['subtract',[2]]]]表示事件類型為tap,觸發(fā)函數(shù)有兩個(gè),一個(gè)為add函數(shù)且無(wú)參數(shù),一個(gè)為subtract且參數(shù)為2。 ['touchstart',[['mixin',['$event']]]]表示事件類型為touchstart,觸發(fā)函數(shù)有一個(gè)為mixin,參數(shù)為$event對(duì)象。

b. 編譯后的js的代碼:

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

這里其實(shí)就是后調(diào)用uni-mp-toutiao里的createPage對(duì)vue的script部分進(jìn)行了初始化。 createPage返回小程序的Component構(gòu)造器,之后是一層層的調(diào)用parsePage、parseBasePage、parseComponent、parseBaseComponent,parseBaseComponent最后返回一個(gè)Component構(gòu)造器

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
}

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

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

  • 拿到點(diǎn)擊元素上的data-event-opts屬性:[['tap',[['add'],['subtract',[2]]]],['touchstart',[['mixin',['$event']]]]]
  • 根據(jù)點(diǎn)擊類型獲取相應(yīng)數(shù)組,比如bindTap就取['tap',[['add'],['subtract',[2]]]]bindtouchstart就取['touchstart',[['mixin',['$event']]]]
  • 依次調(diào)用相應(yīng)事件類型的函數(shù),并傳入?yún)?shù),比如tap調(diào)用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é)點(diǎn)模擬 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ù)據(jù)同步機(jī)制

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

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

// 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ù)據(jù)
      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);
    }
  }
};

源代碼比較簡(jiǎn)單,就是比對(duì)更新前后的數(shù)據(jù),然后獲得diffData,最后批量調(diào)用setData更新數(shù)據(jù)。

3. diff算法

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

  • 類型改變
  • 減量更新
  • 增量更新
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); //列表項(xiàng)新增記錄
        this.setData({
            list:this.data.list
        })
    }
})

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

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

這種寫(xiě)法的開(kāi)發(fā)體驗(yàn)極差,而且不便于維護(hù),所以u(píng)ni-app借鑒了westore JSON Diff的原理,在setData時(shí)進(jìn)行了差量更新,下面,讓我們通過(guò)源碼,來(lái)了解diff的原理吧。

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

function _diff(current, pre, path, result) {
    if (current === pre) {
       // 更新前后無(wú)改變
      return;
    }
    var rootCurrentType = type(current);
    var rootPreType = type(pre);
    if (rootCurrentType == OBJECTTYPE) {
      // 1.對(duì)象類型
      if (rootPreType != OBJECTTYPE || Object.keys(current).length < Object.keys(pre).length) {
        // 1.1數(shù)據(jù)類型不一致或者減量更新,直接替換
        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 處理基礎(chǔ)類型
            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 處理對(duì)象類型
            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遍歷對(duì)象/數(shù)據(jù)類型
        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);
    }
  },
  • 當(dāng)數(shù)據(jù)發(fā)生改變時(shí),uni-app會(huì)將新舊數(shù)據(jù)進(jìn)行比對(duì),然后獲得差量更新的數(shù)據(jù),調(diào)用setData更新。
  • 通過(guò)cur === pre進(jìn)行判斷,相同則直接返回。
  • 通過(guò)type(cur) === OBJECTTYPE進(jìn)行對(duì)象判斷:
    • pre不是OBJECTTYPE或者cur長(zhǎng)度少于pre,則是類型改變或者減量更新,調(diào)用setResult直接添加新數(shù)據(jù)。
    • 否則執(zhí)行增量更新邏輯:
      • 遍歷cur,對(duì)每個(gè)key批量調(diào)用loop函數(shù)進(jìn)行處理。
      • currentType不是ARRAYTYPE或者OBJECTTYPE,則是類型改變,調(diào)用setResult直接添加新數(shù)據(jù)。
      • currentTypeARRAYTYPE
        • preType不是ARRAYTYPE,或者currentValue長(zhǎng)度少于preValue,則是類型改變或者減量更新,調(diào)用setResult直接添加新數(shù)據(jù)。
        • 否則執(zhí)行增量更新邏輯,遍歷currentValue,執(zhí)行_diff進(jìn)行遞歸。
      • currentTypeOBJECTTYPE:
        • preType不是OBJECTTYPE或者currentValue長(zhǎng)度少于preValue,則是類型改變或者減量更新,調(diào)用setResult直接添加新數(shù)據(jù)。
        • 否則執(zhí)行增量更新邏輯,遍歷currentValue,執(zhí)行_diff進(jìn)行遞歸。
  • 通過(guò)type(cur) === ARRAYTYPE進(jìn)行數(shù)組判斷:
    • preType不是OBJECTTYPE或者currentValue長(zhǎng)度少于preValue,則是類型改變或者減量更新,調(diào)用setResult直接添加新數(shù)據(jù)。
    • 否則執(zhí)行增量更新邏輯,遍歷currentValue,執(zhí)行_diff進(jìn)行遞歸。
  • 若以上三個(gè)判斷居不成立,則判斷是基礎(chǔ)類型,調(diào)用setResult添加新數(shù)據(jù)。 小結(jié):_diff的過(guò)程,主要進(jìn)行對(duì)象、數(shù)組和基礎(chǔ)類型的判斷。只有基本類型、類型改變、減量更新會(huì)進(jìn)行setResult,否則進(jìn)行遍歷遞歸_diff。

六.對(duì)比

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

七.總結(jié)

七.參考資料

uni-app官網(wǎng)

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

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

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

總結(jié)

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

相關(guān)文章

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

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

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

    JavaScript 對(duì)象成員的可見(jiàn)性說(shuō)明

    與java等基于類的面向?qū)ο笳Z(yǔ)言的private、protected、public等關(guān)鍵字的用途類似,基于對(duì)象的JavaScript語(yǔ)言,在對(duì)象構(gòu)造上也存在類似的成員可見(jiàn)性問(wèn)題。
    2009-10-10
  • 微信小程序?qū)崿F(xiàn)藍(lán)牙設(shè)備搜索及連接功能示例詳解

    微信小程序?qū)崿F(xiàn)藍(lán)牙設(shè)備搜索及連接功能示例詳解

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

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

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

    詳解JavaScript的垃圾回收機(jī)制

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

    p5.js臨摹旋轉(zhuǎn)愛(ài)心

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

    js生成隨機(jī)數(shù)的過(guò)程解析

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

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

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

    微信小程序Echarts覆蓋正常組件問(wèn)題解決

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

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

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

最新評(píng)論