当前位置:网站首页>How does uni app build applets?

How does uni app build applets?

2022-06-09 23:00:00 Night runner

  Article transferred from :uni-app How to build applets ? - Nuggets

I recommend reading the original .

uni-app It's based on Vue.js Front end framework of syntax development applet , Developers write a set of code , Can be posted to iOS、Android、Web And various small program platforms . today , We analyze relevant cases uni-app How to put Vue.js Built into a native applet .

Vue yes template、script、style Three paragraph SFC,uni-app How to put SFC Split into small programs ttml、ttss、js、json Four stage ? With questions , This article will start from webpack、 compiler 、 The runtime takes you through three aspects uni-app How to build applets .

One . usage

uni-app Is based on vue-cli Scaffolding development , Integrate a remote Vue Preset

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

uni-app At present, many different project templates are integrated , According to different needs , Choose a different template

function 、 Release uni-app, Take byte applet for example

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

Two . principle

uni-app Is a more traditional small program framework , Include compiler + Runtime . Applet is a two-threaded architecture with separate view and logic layers , The loading and running of views and logic do not block each other , meanwhile , Logical layer data updates drive view layer updates , Event response of the view , It will trigger the interaction of the logic layer . uni-app The source code mainly includes three aspects :

  1. webpack.webpack It is a module packer commonly used in the front end ,uni-app During construction , Will Vue SFC Of template、script、style A three-stage structure , Compile into a four segment structure of a small program , Take byte applet for example , You'll get ttml、ttss、js、json Four kinds of documents .
  2. compiler .uni-app The essence of the compiler is to Vue The view of is compiled into the view of the applet , Namely the template Syntax compiled into small programs ttml grammar , after ,uni-app View layers are not maintained , The update of the view layer is completely left to the applet itself . however uni-app It's using Vue developable , that Vue How does it interact with applets ? It depends on uni-app Runtime .
  3. Runtime . When running, it is equivalent to a bridge , Opened the Vue And small program . Update of applet view layer , For example, event Click 、 Touch and other operations , It will go through the event proxy mechanism at runtime , Then arrive at Vue Event function of . and Vue The event function of triggers the data update , It will go through the runtime again , Trigger setData, Further update the view layer of the applet .

remarks : The source code of this article is uni-app ^2.0.0-30720210122002 edition .

3、 ... and .webpack

1. package.json

First look at package.json scripts command :

  • Inject NODE_ENV and UNI_PLATFORM command
  • call vue-cli-service command , perform uni-build command
"dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch",
 Copy code 

2. entrance

When we run inside the project vue-cli-service On command , It parses and loads automatically package.json All listed in CLI plug-in unit ,Vue CLI The name of the plug-in follows vue-cli-plugin- perhaps @scope/vue-cli-plugin- The specification of , The main plug-ins here are @dcloudio/vue-cli-plugin-uni, Related to the source code :

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 Function will get webpack Configure and execute 
    await build(args, api, options)

    delete process.env.VUE_CLI_BUILD_TARGET
  })
}
 Copy code 

When we execute UNI_PLATFORM=mp-toutiao vue-cli-service uni-build when ,@dcloudio/vue-cli-plugin-uni Just did two things :

  • Get the webpack To configure .
  • perform uni-build On command , And then execute webpack.

therefore , The entry file is actually the execution webpack,uni-app Of webpack The configuration is mainly located in @dcloudio/vue-cli-plugin-uni/lib/mp/index.js, Next we pass entry、output、loader、plugin Let's see uni-app How to put Vue SFC Convert to applet .

3. Entry

uni-app Would call parseEntry Parse pages.json, And then on process.UNI_ENTRY

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

Let's take a look at parseEntry Main code :

function parseEntry (pagesJson) {
  //  There is an entry by default 
  process.UNI_ENTRY = {
    'common/main': path.resolve(process.env.UNI_INPUT_DIR, getMainEntry())
  }

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

  //  add to pages entrance 
  pagesJson.pages.forEach(page => {
    process.UNI_ENTRY[page.path] = getMainJsPath(page.path)
  })
}

function getPagesJson () {
  //  obtain pages.json To analyze 
  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}   Must return a  json  object `)
      }
    } else {
      console.error(`${pagesJsonJsFileName}  Must export  function`)
    }
  }
  //  Check whether the configuration is legal 
  filterPages(pagesJson.pages)
  return pagesJson
}

function getMainJsPath (page) {
  //  take main.js and page Parameters are combined to form a new entrance 
  return path.resolve(process.env.UNI_INPUT_DIR, getMainEntry() + '?' + JSON.stringify({
    page: encodeURIComponent(page)
  }))
}
 Copy code 

parseEntry The main work of :

  • Configure the default entry main.js
  • analysis pages.json, take page As a parameter , and main.js Form a new entrance

such as , our pages.json The contents are as follows :

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "uni-app"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  }
}
 Copy code 

Then let's look at the output enrty, It can be found that it is actually through main.js Distinguish with response parameters page Of , This one vue-loader distinguish template、script、style In fact, it's very similar to , You can judge the parameters later , Different calls loader To deal with .

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

4. Output

The output is relatively simple ,dev and build Packed separately to dist/dev/mp-toutiao and dist/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',
    }
}
 Copy code 

5. Alias

uni-app There are two main alias To configure

  • vue$ It's a vue Replace with uni-app Of mp-vue
  • uni-pages Express pages.json file
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'
},
 Copy code 

6. Loader

From the above we can see entry All are main.js, Just bring it page Parameters of , We start at the entrance , look down uni-app How to process files step by step , Take a look at the treatment first main.js Of the two loader:lib/main and wrap-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'
      }]
    }]
}
 Copy code 

a. lib/main:

Let's look at the core code , according to resourceQuery Parameters , We mainly look at query The situation of , Will be introduced here Vue and pages/index/index.vue, At the same time call createPage To initialize ,createPage It's runtime , I'll talk about it later . By introducing .vue, So the subsequent analysis is handed over to 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' To trigger  vendor  Merge 
      let ext = '.vue'
      return this.callback(null,
        `
import Vue from 'vue'
import Page from './${normalizePath(params.page)}${ext}'
createPage(Page)
`, map)
    }
  }    else    {......}
}
 Copy code 

b. wrap-loader:

Introduced uni-pages, from alias But I know it is import pages.json, about pages.json,uni-app There are also specialized webpack-uni-pages-loader To deal with .

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

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

c. webpack-uni-pages-loader:

More code , Let's post the general core code , Look at the main things done

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

  //  analysis 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) {
        //  For the resolved app.json and project.config.json Cache 
        if (jsonFile.name === 'app') {
          // updateAppJson and updateProjectJson In fact, it's called 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')
}

//  to update json file 
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
      //  In fact, take the new one directly  merge  The old one should be OK 
      const oldJsonObj = JSON.parse(oldJsonStr)
      jsonObj.usingComponents = oldJsonObj.usingComponents || {}
      jsonObj.usingAutoImportComponents = oldJsonObj.usingAutoImportComponents || {}
      if (oldJsonObj.usingGlobalComponents) { //  Copy  global components( Global... Is not supported for  usingComponents  The platform of )
        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)
}
 Copy code 

We learn about... Step by step webpack-uni-pages-loader The role of :

  1. obtain mainfest.json and pages.json The content of
  2. Respectively called updateAppJson and updateProjectJson Handle mainfest.json and page.json
  3. updateAppJson and updateProjectJson The essence is to call updateComponentJson,updateComponentJson Will update json file , The final call updateJsonFile
  4. updateJsonFile yes json Key points for file generation . First, we will define a shared jsonFileMap Key value object , Then there is no direct generation of the corresponding json file , But the mainfest.json and page.json Processing into project.config and app, Then cache in jsonFileMap in .
  5. Why not directly generate ? Because later pages/index/index.vue There will be json File generation , So all of them json Files are temporarily cached in jsonFileMap in , Follow up plugin Unified generation .

Popular said ,webpack-uni-pages-loader The function is json The conversion of grammar , And then there's caching , Syntax conversion is simple , It's just the object key value Change of , We can intuitively compare mainfest.json and page.json Differences before and after construction .

//  Before conversion page.json
{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "uni-app"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  }
}
//  What you get from the conversion is app.json
{
  "pages": [
    "pages/index/index"
  ],
  "subPackages": [],
  "window": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  },
  "usingComponents": {}
}

//  Before conversion mainfest.json
{
  "name": "",
  "appid": "",
  "description": "",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": true
}

//  What you get from the conversion is project.config.json
{
  "setting": {
    "urlCheck": true,
    "es6": false,
    "postcss": false,
    "minified": false,
    "newFeature": true
  },
  "appid": " Experience appId",
  "projectname": "uniapp-analysis"
}
 Copy code 

d. vue-loader:

processed js and json file , And here we are vue Handling of documents ,vue-loader Will be able to vue Split into template、style、script. about style, In fact, that is css, Pass by less-loadersass-loaderpostcss-loadercss-loader To deal with , Finally by mini-css-extract-plugin Generate corresponding .ttss file . about script,uni-app The main configuration is script loader To deal with , This process is mainly to index.vue The components introduced in are separated into index.json, And then with app.json equally , cached jsonFileMap Array .

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

about template, This is the core module ,uni-app To change the vue-loader Of compiler, take vue-template-compiler replaced uni-template-compiler,uni-template-compiler Is used to put vue Syntax to applet syntax , Here we can remember , How to compile will be discussed later . Here we focus on the processing template Of 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'
  }]
}
 Copy code 

loader lib/template First, I will get vueLoaderOptions, And then add new options, The key to the applet is emitFile, because vue-loader In itself, there is no going compiler Inject emitFile Of , therefore compiler The compiled syntax should generate ttml Need to have 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
  })
}
 Copy code 

7. plugin

uni-app The main plugin yes createUniMPPlugin, This process corresponds to our loader Handle json Generated on jsonFileMap object , The essence is to jsonFileMap Inside json Generate real files .

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) => {
          //  Generate .json
          generateJson(compilation)
          //  Generate app.json、project.config.json
          generateApp(compilation)
            .forEach(({
              file,
              source
            }) => emitFile(file, source, compilation))

          resolve()
        })
      })
    }
 Copy code 

Related global configuration variables

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

Four . The compiler knows one or two

The principle of the compiler is actually through ast Grammar analysis of , hold vue Of template Syntax conversion to applet ttml grammar . But it's actually very abstract , How to get through ast Grammar ? Next , We build a simple version of template=>ttml The compiler , Realization div=>view Label conversion for , To get to know uni-app The compilation process of .

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

Above this template after uni-app After compilation, it will become the following code , Look, it's just div => view Replacement , But in fact, there are many processes in the process .

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

1. vue-template-compiler

First ,template Pass by vue The compiler , Get the rendering function render.

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

2. @babel/parser

This step is to take advantage of parser take render Function to ast.ast yes Abstract syntax tree Abbreviation , That is, abstract syntax tree .

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

Here we filter out some start、end、loc、errors Etc. will affect the fields we read ( complete ast Can pass astexplorer.net Website view ), Look at the translated ast object , The json Object we focus on program.body[0].expression. 1.type There are four types of :

  • CallExpression( Call expression ):_c()
  • StringLiteral( Literal of a string ):'div'
  • ObjectExpression( Object expression ):'{}'
  • ArrayExpression( Array expression ):[_v("hello world!")]

2.callee.name Is the name of the calling expression : Here you are _c、_v Two kinds of 3.arguments.*.value Is the value of the parameter : Here you are div、text、hello world! We put ast Objects and render Function comparison , It is not difficult to find that these two are actually one-to-one reversible relations .

{
  "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": []
}
 Copy code 

3. @babel/traverse and @babel/types

This step mainly uses traverse Pair generated ast Object to traverse , And then use it types Judge and modify ast The grammar of . traverse(ast, visitor) There are two main parameters :

  • parser Resolved ast
  • visitor:visitor Is a variety of type Or is it enter and exit The object of composition . Here we specify CallExpression type , Traverse ast When I met CallExpression Type will execute the function , Put the corresponding div、img Convert to view、image.

Other types can be seen in the document :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);
 Copy code 

4. Generate vnode

uni-app Generate applet ttml It is necessary to modify the ast Generate similar vNode The object of , And I'm going to go through it again vNode Generate ttml.

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

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

//  Different element Go through different creation functions 
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));
    }, []);
  }
}

//  transformation style attribute 
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;
}

//  establish Text Text node 
function traverseCreateTextVNode(callExprNode) {
  const arg = callExprNode.arguments[0];
  if (t.isStringLiteral(arg)) {
    return arg.value;
  }
}

//  establish element node 
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;
}
 Copy code 

The reason why it is not used here @babel/generator, Because of the use of generator What is generated is still render function , Although the grammar has been modified , But according to render There is no way to directly generate small programs ttml, It still has to be converted into vNode. best , Let's take a look at the generated VNode object .

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

5. Generate code

Traverse VNode, Generate applet code recursively

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}>`;
}
 Copy code 

6. Overall process :

Here's a list uni-template-compiler Roughly converted process and key code ,uni-template-compiler Of ast Grammar transformation is all about traverse This process is completed .

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

5、 ... and . The principle of runtime

uni-app Provides a runtime uni-app runtime, Packaged into the final running applet distribution code , The runtime implements Vue.js And the data between the two systems of the applet 、 Event synchronization .

1. Event agent

Let's take a number increase as an example , have a look uni-app How to put the data of the applet 、 Event with vue Integrated .

<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>
 Copy code 

a. The compiled ttml, Compiled here data-event-opts、bindtap Just like the previous compiler div => view The principle is similar , Also in traverse It's done ast transformation , Let's look directly at the generated after compilation ttml:

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

Here we first analyze data-event-opts Array : data-event-opts Is a two-dimensional array , Each subarray represents an event type . Subarray has two values , The first represents the event type name , The second one represents the number of trigger event functions . The event function is an array , The first value represents the event function name , The second is the number of parameters . ['tap',[['add'],['subtract',[2]]]] Indicates that the event type is tap, There are two trigger functions , One for add Function with no arguments , One for subtract And the parameter is 2. ['touchstart',[['mixin',['$event']]]] Indicates that the event type is touchstart, The trigger function has a mixin, Parameter is $event object .

b. The compiled js Code for :

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

This is actually the post call uni-mp-toutiao Inside createPage Yes vue Of script Partially initialized . createPage Returns the... Of the applet Component Constructors , Then there are layer upon layer calls parsePageparseBasePageparseComponentparseBaseComponent,parseBaseComponent The last one to return Component Constructors

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
}
 Copy code 

We directly compare the before and after conversion vue and mp Parameter differences , In itself vue Grammar and mp Component The grammar of is very similar to . here ,uni-app Will be able to vue Of data Properties and methods Method copy To mp Of data, and mp Of methods There are mainly __e Method .

Back to compiler generation ttml Code , All events found will call __e event , and __e The corresponding is handleEvent event , Let's take a look at handleEvent

  1. Get the... On the click element data-event-opts attribute :[['tap',[['add'],['subtract',[2]]]],['touchstart',[['mixin',['$event']]]]]
  2. Get the corresponding array according to the click type , such as bindTap Just take ['tap',[['add'],['subtract',[2]]]],bindtouchstart Just take ['touchstart',[['mixin',['$event']]]]
  3. Call the function of the corresponding event type in turn , And pass in parameters , such as tap call 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']; //  Alipay  web-view  Components  dataset  Non hump 

  // [['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  Abstract node simulation  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])));
        }
      });
    }
  });
}
 Copy code 

2. Data synchronization mechanism

Applet view layer event response , Will trigger the applet logic event , The logic layer calls vue Corresponding events , Trigger data update . that Vue After the data update , And how to trigger the update of the applet view layer ?
Applet data update must call the applet's setData function , and Vue When the data is updated, it will trigger Vue.prototype._update Method , therefore , As long as _update Call inside setData Function is OK . uni-app stay Vue New in patch function , This function will be in _update When called .

// 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) { // Synchronous only  data  There's some data in 
      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 +
          '] Differential update ',
          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);
    }
  }
};
 Copy code 

The source code is relatively simple , Is to compare the data before and after the update , Then get diffData, Finally, batch call setData Update data .

3. diff Algorithm

There are three situations for updating applet data

  1. Type change
  2. Decrement update
  3. Incremental updating
page({
    data:{
        list:['item1','item2','item3','item4']
    },
    change(){
        // 1. Type change 
        this.setData({
            list: 'list'
        })
    },
    cut(){
        // 2. Decrement update 
        let newData = ['item5', 'item6'];
        this.setData({
            list: newData
        })
    },
    add(){
        // 3. Incremental updating 
        let newData = ['item5','item6','item7','item8'];
        this.data.list.push(...newData); // List item add record 
        this.setData({
            list:this.data.list
        })
    }
})
 Copy code 

For type replacement or decrement update , We just need to replace the data directly , But for incremental updates , In case of direct data replacement , There will be some performance problems , Take the example above , take item1~item4 Update to item1~item8, This process we need 8 All the data will be passed on , But in practice, it is only updated item5~item8. under these circumstances , In order to optimize performance , We must use the following expression , Manual incremental update :

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

The development experience of this way of writing is very poor , And it's not easy to maintain , therefore uni-app Learn from it westore JSON Diff Principle , stay setData The difference is updated , below , Let's go through the source code , To get to know diff Why not .

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

function _diff(current, pre, path, result) {
    if (current === pre) {
       //  There is no change before and after the update 
      return;
    }
    var rootCurrentType = type(current);
    var rootPreType = type(pre);
    if (rootCurrentType == OBJECTTYPE) {
      // 1. object type 
      if (rootPreType != OBJECTTYPE || Object.keys(current).length < Object.keys(pre).length) {
        // 1.1 Inconsistent data types or decrement updates , Direct replacement 
        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  Handling foundation types 
            if (currentValue != pre[key]) {
              setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
            }
          } else if (currentType == ARRAYTYPE) {
            // 1.2.2  Handle array types 
            if (preType != ARRAYTYPE) {
              //  Different types 
              setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
            } else {
              if (currentValue.length < preValue.length) {
                //  Decrement update 
                setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
              } else {
                //  Incremental updates are recursive 
                currentValue.forEach(function (item, index) {
                  _diff(item, preValue[index], (path == '' ? '' : path + '.') + key + '[' + index + ']', result);
                });
              }
            }
          } else if (currentType == OBJECTTYPE) {
            // 1.2.3  Handle object types 
            if (preType != OBJECTTYPE || Object.keys(currentValue).length < Object.keys(preValue).length) {
              //  Different types / Decrement update 
              setResult(result, (path == '' ? '' : path + '.') + key, currentValue);
            } else {
              //  Incremental updates are recursive 
              for (var subKey in currentValue) {
                _diff(
                  currentValue[subKey],
                  preValue[subKey],
                  (path == '' ? '' : path + '.') + key + '.' + subKey,
                  result
                );
              }
            }
          }
        };
        // 1.2 Traversing objects / data type 
        for (var key in current) loop(key);
      }
    } else if (rootCurrentType == ARRAYTYPE) {
      // 2. An array type 
      if (rootPreType != ARRAYTYPE) {
        //  Different types 
        setResult(result, path, current);
      } else {
        if (current.length < pre.length) {
          //  Decrement update 
          setResult(result, path, current);
        } else {
          //  Incremental updates are recursive 
          current.forEach(function (item, index) {
            _diff(item, pre[index], path + '[' + index + ']', result);
          });
        }
      }
    } else {
      // 3. Basic types 
      setResult(result, path, current);
    }
  },
 Copy code 
  • When data changes ,uni-app Will compare the old and new data , Then get the data updated by the difference , call setData to update .
  • adopt cur === pre Judge , The same is returned directly .
  • adopt type(cur) === OBJECTTYPE Make object judgment :
    • if pre No OBJECTTYPE perhaps cur Less than pre, Type change or decrement update , call setResult Add new data directly .
    • Otherwise, execute incremental update logic :
      • Traverse cur, For each key Batch call loop Function to process .
      • if currentType No ARRAYTYPE perhaps OBJECTTYPE, Is the type change , call setResult Add new data directly .
      • if currentType yes ARRAYTYPE
        • if preType No ARRAYTYPE, perhaps currentValue Less than preValue, Type change or decrement update , call setResult Add new data directly .
        • Otherwise, execute incremental update logic , Traverse currentValue, perform _diff Recursion .
      • if currentType yes OBJECTTYPE:
        • if preType No OBJECTTYPE perhaps currentValue Less than preValue, Type change or decrement update , call setResult Add new data directly .
        • Otherwise, execute incremental update logic , Traverse currentValue, perform _diff Recursion .
  • adopt type(cur) === ARRAYTYPE Perform array judgment :
    • if preType No OBJECTTYPE perhaps currentValue Less than preValue, Type change or decrement update , call setResult Add new data directly .
    • Otherwise, execute incremental update logic , Traverse currentValue, perform _diff Recursion .
  • If the above three judgments are not true , Then the judgment is the basic type , call setResult Add new data .

Summary :_diff The process of , Mainly for the object 、 Array and basic type judgment . Only basic types 、 Type change 、 The decrement update will take place setResult, Otherwise, the traversal recursion _diff.

6、 ... and . contrast

uni-app Is a compiled framework , Although there are many operational frameworks on the market , such as Rax Runtime /Remax/Taro Next. Compare these ,uni-app The disadvantage of this kind of compiled framework is syntax support , A running framework has almost no syntax restrictions , and uni-app because ast Complexity and convertibility , As a result, some syntax cannot be supported . But the runtime has its drawbacks , The runtime uses the template syntax of the applet template, and uni-app use Component Constructors , Use Component The advantage is that the native framework can know the general structure of the page , and template Template rendering cannot be done , meanwhile , The data transmission capacity of the running framework is large , You need to convert the data into VNode Transfer a view layer , This is also the cause of operational performance loss .

7、 ... and . summary

7、 ... and . Reference material

uni-app Official website
The front end is cross stack | Baugur - How to Polish uni-app High performance and ease of use of the cross end framework · Language sparrow
The front end is cross stack |JJ- How to use Taro Next Across cross end lines of business · Language sparrow
stay 2020 year , How to choose a small program framework


author :luocheng
link :https://juejin.cn/post/6968438754180595742
source : Rare earth digs gold
The copyright belongs to the author . Commercial reprint please contact the author for authorization , Non-commercial reprint please indicate the source .

原网站

版权声明
本文为[Night runner]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/160/202206092212007437.html