当前位置:网站首页>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 :
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 .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 .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-servicecommand , performuni-buildcommand
"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
webpackTo configure . - perform
uni-buildOn command , And then executewebpack.
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-vueuni-pagesExpress 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 :
- obtain
mainfest.jsonandpages.jsonThe content of - Respectively called
updateAppJsonandupdateProjectJsonHandlemainfest.jsonandpage.json updateAppJsonandupdateProjectJsonThe essence is to callupdateComponentJson,updateComponentJsonWill updatejsonfile , The final call updateJsonFileupdateJsonFileyesjsonKey points for file generation . First, we will define a sharedjsonFileMapKey value object , Then there is no direct generation of the correspondingjsonfile , But themainfest.jsonandpage.jsonProcessing intoproject.configandapp, Then cache injsonFileMapin .- Why not directly generate ? Because later
pages/index/index.vueThere will bejsonFile generation , So all of themjsonFiles are temporarily cached injsonFileMapin , Follow uppluginUnified 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-loader、sass-loader、postcss-loader、css-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 :
parserResolved astvisitor: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 parsePage、parseBasePage、parseComponent、parseBaseComponent,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:
- Get the... On the click element
data-event-optsattribute :[['tap',[['add'],['subtract',[2]]]],['touchstart',[['mixin',['$event']]]]] - Get the corresponding array according to the click type , such as
bindTapJust take['tap',[['add'],['subtract',[2]]]],bindtouchstartJust take['touchstart',[['mixin',['$event']]]] - Call the function of the corresponding event type in turn , And pass in parameters , such as
tapcallthis.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
- Type change
- Decrement update
- 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 === preJudge , The same is returned directly . - adopt
type(cur) === OBJECTTYPEMake object judgment :- if
preNoOBJECTTYPEperhapscurLess thanpre, Type change or decrement update , callsetResultAdd new data directly . - Otherwise, execute incremental update logic :
- Traverse
cur, For each key Batch callloopFunction to process . - if
currentTypeNoARRAYTYPEperhapsOBJECTTYPE, Is the type change , callsetResultAdd new data directly . - if
currentTypeyesARRAYTYPE:- if
preTypeNoARRAYTYPE, perhapscurrentValueLess thanpreValue, Type change or decrement update , callsetResultAdd new data directly . - Otherwise, execute incremental update logic , Traverse
currentValue, perform_diffRecursion .
- if
- if
currentTypeyesOBJECTTYPE:- if
preTypeNoOBJECTTYPEperhapscurrentValueLess thanpreValue, Type change or decrement update , callsetResultAdd new data directly . - Otherwise, execute incremental update logic , Traverse
currentValue, perform_diffRecursion .
- if
- Traverse
- if
- adopt
type(cur) === ARRAYTYPEPerform array judgment :- if
preTypeNoOBJECTTYPEperhapscurrentValueLess thanpreValue, Type change or decrement update , callsetResultAdd new data directly . - Otherwise, execute incremental update logic , Traverse
currentValue, perform_diffRecursion .
- if
- 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 .
边栏推荐
- 经典面试题:如何快速求解根号2?
- 【图像去噪】基于高斯、均值、中值、双边滤波实现图像去噪含Matlab源码
- 目前28岁,从20年开始北漂闯荡到入职软件测试,我成功捧起15K薪资
- 关于mongodb的那些安装、配置、报错处理、CRUD操作等再总结
- Please ask a question, Flink SQL. Do you want to specify a time point to start reading data? Can we do this?
- STM32驱动继电器 STM32F103RCT6基于寄存器和库函数驱动IO口
- 【刷题篇】最长递增子序列
- 后疫情时代裸辞后面试软件测试工程师被拒,写下一些感悟和面试心得
- GUI user login
- IPSec的特征与功能
猜你喜欢

Im instant messaging development: mobile protocol UDP or TCP?

【卷指南】Mendeley文献管理工具教程
![[Blue Bridge Cup training 100 questions] scratch vertigo Apple Blue Bridge Cup scratch competition special prediction programming questions collective training simulation exercises question 12](/img/96/330245fa4f594fe94c39e419001d3a.png)
[Blue Bridge Cup training 100 questions] scratch vertigo Apple Blue Bridge Cup scratch competition special prediction programming questions collective training simulation exercises question 12

MongoDB的使用及CRUD操作

最新活动|OpenHarmony开源开发者成长计划解决方案学生挑战赛即将开启!

Repeated web1 history in Web3

lua学习笔记(4)-- 搭建mobdebug 远程开发环境

目前28岁,从20年开始北漂闯荡到入职软件测试,我成功捧起15K薪资
![[the second revolution of report tools] optimize report structure and improve report operation performance based on SPL language](/img/53/d6f05e8050e27dc9d59f1196753512.png)
[the second revolution of report tools] optimize report structure and improve report operation performance based on SPL language

从零开始实现lmax-Disruptor队列(二)多消费者、消费者组间消费依赖原理解析
随机推荐
原来树状数组可以这么简单?
关于mongodb的那些安装、配置、报错处理、CRUD操作等再总结
Metauniverse may replace the Internet as the mainstream lifestyle in the next decade or even two
Leetcode(力扣)超高頻題講解(一)
Classic interview question: how to quickly solve root 2?
Ffmpeg plus opencv face collection and recognition of the actual project!
Common embedded end streaming media server open source projects!
MongoDB的使用及CRUD操作
Laravel 上传文件信息获取
Explanation of leetcode UHF questions (III)
【刷题篇】跳跃游戏
be all eagerness to see it! Data list of Benji Banas' first quarter reward activities!
Experience in database optimization
Re summary of mongodb installation, configuration, error handling, crud operation, etc
常见的嵌入式端流媒体服务器开源项目!
[Blue Bridge Cup training 100 questions] scratch vertigo Apple Blue Bridge Cup scratch competition special prediction programming questions collective training simulation exercises question 12
元宇宙或将会取代互联网成为下一个十年,甚至二十年人们主流的生活方式
从零开始实现lmax-Disruptor队列(二)多消费者、消费者组间消费依赖原理解析
房贷利率下调 现在是买房的时机吗?
How to pass the probation period for new programmers