Without further ado, let me give you a brief description of what we did. The image may be too blurry, so click on SVG

background

Recently, the company started the business of small program, and I was assigned to take charge of this business. One of the problems we need to deal with is access to our traditional web development architecture — modular development. Let’s talk more about what modular development looks like. Git flow is used in our Git workflow. A project will be divided into several modules, and one person will be responsible for the independent development of one module (corresponding to a feature of Git flow). Modules are developed and connected to the back end, then merged into Develop for integration testing, and then released after a series of tests. The directory structure is shown in the figure. A module contains its own pages/Components/Assets/Model/mixins/apis/routes/SCSS and so on.

The benefits of this development mode are self-evident, everyone can develop in parallel, greatly increasing the speed of development. This is to transplant this development model into small programs.

The target

So with that background, let’s clarify our goals. I use the WEPY framework, like vUE syntax development, development experience is very good. In vUE, a component is a single file that contains JS, HTML, and CSS. Wepy uses the syntax of Vue, but slightly different from Vue, wepy components are divided into three classes –wepy.app, wepy.page, and wepy.component.component.html. Corresponding to our directory structure, each module is actually a series of Page components. To combine this set of modules, simply scan the routes from the set of pages into a routing table and insert it into the entry of the applet –app.json. The corresponding wepy framework is the Pages field in app.wpy.

Scanning the Routing Table

The first step! First get all pages routes and synthesize into a routing table! My solution is to create a new routes file in each module, which is equivalent to registering every page that needs to be inserted into the entry. The page that does not need to access the service does not need to be registered. Vue – Router registration syntax

//routes.js
module.exports = [
    {
        name: 'home-detail'.//TODO:Name occupies space first, and then attempts to jump to a page by reading name
        page: 'detail'.// The file name of the page that needs to be accessed. For example, here is index.wpy. The path to SRC/is' modules/${moduleName}/pages/index '.
    },
    {
        name: 'home-index'.page: 'index'.meta: {
            weight: 100// A small function is added here, because the applet specifies that the first item in the Pages array is the home page, I will use this weight field to sort the pages route. The higher the weight, the higher the position.}}]Copy the code

The script that scans the modules and merges the routing tables is simple enough to read and write files.

const fs = require('fs')
const path = require('path')

const routeDest = path.join(__dirname, '.. /src/config/routes.js')
const modulesPath = path.join(__dirname, '.. /src/modules')

let routes = []

fs.readdirSync(modulesPath).forEach(module= > {
    if(module.indexOf('.DS_Store') > - 1) return 

    const route = require(`${modulesPath}/The ${module}/route`)
    route.forEach(item= > {
        item.page = `modules/The ${module}/pages/${item.page.match(/ / /? / (. *)) [1]}`
    })
    routes = routes.concat(route)
})

fs.writeFileSync(routeDest,`module.exports = The ${JSON.stringify(routes)}`, e => {
    console.log(e)
})
Copy the code

Routing sorting policy

const strategies = {
    sortByWeight(routes) {
        routes.sort((a, b) = > {
            a.meta = a.meta || {}
            b.meta = b.meta || {}

            const weightA = a.meta.weight || 0
            const weightB = b.meta.weight || 0

            return weightB - weightA
        })
        return routes
    }
}
Copy the code

Finally, the routing table is obtained

const Strategies = require('.. /src/lib/routes-model')
const routes = Strategies.sortByWeight(require('.. /src/config/routes'))
const pages = routes.map(item= > item.page)
console.log(pages)//['modules/home/pages/index', 'modules/home/pages/detail']
Copy the code

Alternate route array

So far so good… The problem is how to replace the route array in the entry file. Here’s what I did.

Directly introducing

My first thought was, isn’t it easy? Before wepy is compiled, run the script to derive the routing table and import the routing table.

import routes from './routes'
export default class extends wepy.app {
  config = {
    pages: routes,//['modules/home/pages/index']
    window: {
      backgroundTextStyle: 'light'.navigationBarBackgroundColor: '#fff'.navigationBarTitleText: 'Hi, I'm Scum Fai.'.navigationBarTextStyle: 'black'}}/ /...
}
Copy the code

The pages field value must be static, configured before the applets run, not dynamically imported! Try it if you don’t believe me. That said, make it important – we must precompile again before wepy compiles – to replace the pages field value first!

Regular matching substitution

If you want to replace it first, you need to precisely locate the value of the Pages field and then replace it. The difficulty is how to pinpoint the value of the Pages field? The most efficient method: regular matching. Pre-set the coding specification to add /* __ROUTES__ */ comments before and after the value of the Pages field

The script is as follows:

const fs = require('fs')
const path = require('path')
import routes from './routes'

function replace(source, arr) {
    const matchResult = source.match(/\/\* __ROUTE__ \*\/([\s\S]*)\/\* __ROUTE__ \*\//)
    if(! matchResult) {throw new Error('Must include /* __ROUTE__ */ tag comment')}const str = arr.reduce((pre, next, index, curArr) = > {
        return pre += ` '${curArr[index]}', `
    }, ' ')
    return source.replace(matchResult[1], str)
}

const entryFile = path.join(__dirname, '.. /src/app.wpy')
let entry = fs.readFileSync(entryFile, {encoding: 'UTF-8'})

entry = replace(entry, routes)

fs.writeFileSync(entryFile, entry)
Copy the code

The changes to app.wpy are as follows:

//before
export default class extends wepy.app {
  config = {
    pages: [
    /* __ROUTE__ */
    /* __ROUTE__ */].window: {
      backgroundTextStyle: 'light'.navigationBarBackgroundColor: '#fff'.navigationBarTitleText: 'Hi, I'm Scum Fai.'.navigationBarTextStyle: 'black'}}/ /...
}
//after
export default class extends wepy.app {
  config = {
    pages: [
/* __ROUTE__ */'modules/home/pages/index'./* __ROUTE__ */].window: {
      backgroundTextStyle: 'light'.navigationBarBackgroundColor: '#fff'.navigationBarTitleText: 'Hi, I'm Scum Fai.'.navigationBarTextStyle: 'black'}}/ /...
}
Copy the code

All right, well, that’s it. Because the project is very urgent, so I first used this plan to develop a week and a half. After the development of total feel this kind of program is too uncomfortable, so plotting to change another kind of accurate automatic program…

The Babel plug-in replaces global constants

1. The train of thought

I’m sure you’re all familiar with this pattern

let host = 'http://www.tanwanlanyue.com/'
if(process.env.NODE_ENV === 'production'){
    host = 'http://www.zhazhahui.com/'
}
Copy the code

We can do a lot of value matching with global constants that only exist during compilation. Because wepy has a precompiled layer, the business code in the framework cannot read the value of process.env.node_env. I was thinking about making a Babel plugin similar to WebPack’s DefinePlugin. The idea is that when Babel accesses the AST during compilation, it matches the identifiers or expressions that need to be replaced, and then replaces the corresponding values. For example, In

export default class extends wepy.app {
  config = {
    pages: __ROUTE__,
    window: {
      backgroundTextStyle: 'light'.navigationBarBackgroundColor: '#fff'.navigationBarTitleText: 'Hi, I'm Scum Fai.'.navigationBarTextStyle: 'black'}}/ /...
}
Copy the code

Out

export default class extends wepy.app {
  config = {
    pages: [
        'modules/home/pages/index',].window: {
      backgroundTextStyle: 'light'.navigationBarBackgroundColor: '#fff'.navigationBarTitleText: 'Hi, I'm Scum Fai.'.navigationBarTextStyle: 'black'}}/ /...
}
Copy the code

2. Learn how to write a Babel plug-in

Babel compilers do three things: parse, convert, and generate. The next step is to write a manual for getting started with the Babel plug-in. It basically covers all aspects of writing plug-ins, but due to the lack of documentation of several tools in Babel, it is necessary to read the comments in the code to read the API usage when writing plug-ins. Then there is the big kill AST converter – astExplorer.net. Let’s look at the document from Babel’s parser, Babylon, which covers so many node types that it’s impossible to draw an AST tree. When I write the script, I put the code inside the converter to generate the AST tree, and then step by step.

Before writing the Babel plug-in, understand the concept of abstract syntax trees. What compilers do can be summarized as: parse, transform, and generate. For a detailed explanation of the concept, it might be better to see the introductory manual. Here are some of my own understandings.

Analysis includes lexical analysis and grammatical analysis. Parse the process. As I understand it, abstract syntax trees are very similar to DOM trees. Lexical analysis is a bit like parsing HTML into dom nodes, and parsing is a bit like describing DOM nodes as DOM trees.

The conversion process is the most complex logic concentration in the compiler. First, understand the concepts of tree traversal and visitor pattern.

The example of tree traversal is given in the manual: Suppose you have code like this:

function square(n) {
  return n * n;
}
Copy the code

Then there is the following tree structure:

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)
Copy the code
  • Enter theFunctionDeclaration
    • Enter theIdentifier (id)
    • Come to an end
    • exitIdentifier (id)
    • Enter theIdentifier (params[0])
    • Come to an end
    • exitIdentifier (params[0])
    • Enter theBlockStatement (body)
      • Enter theReturnStatement (body)
        • Enter theBinaryExpression (argument)
          • Enter theIdentifier (left)
          • exitIdentifier (left)
          • Enter theIdentifier (right)
          • exitIdentifier (right)
        • exitBinaryExpression (argument)
      • exitReturnStatement (body)
    • exitBlockStatement (body)

The “visitor pattern” can be understood as a method that is called when a node is entered. For example, you have the following visitors:

const idVisitor = {
  IdentifierConsole.log () {console.log() {console.log() {console.log() {console.log() {console.log() {console.log()"visit an Identifier")}}Copy the code

Combining tree traversal means that each visitor has two opportunities to enter and exit a node. The key to our constant replacement plugin is to access a node by identifying the node as our target and then replacing its value!

3. Write plug-ins

Without further ado, get right to the code. One tool used here is babel-types, which checks nodes.

The difficulty is not that big, the main job is to be familiar with how to match the target node. For example, the matching pattern method is used when matching memberExpression, and the matching identifier directly checks the node name and other routines. The final product and usage can be found on my Github

const memberExpressionMatcher = (path, key) = > path.matchesPattern(key)// Matching conditions for complex expressions
const identifierMatcher = (path, key) = > path.node.name === key// The matching condition of the identifier

const replacer = (path, value, valueToNode) = > {// Replace the utility function for the operation
    path.replaceWith(valueToNode(value))

    if(path.parentPath.isBinaryExpression()){Var isProp = __ENV__ === 'production' ===> var isProp = true
        const result = path.parentPath.evaluate()
        if(result.confident){
            path.parentPath.replaceWith(valueToNode(result.value))
        }
    }
}

export default function ({ types: t }){// Use the tool babel-types
    return {
        visitor: {
            MemberExpression(path, { opts: params }){// Match complex expressions
                Object.keys(params).forEach(key= > {/ / traverse the Options
                    if(memberExpressionMatcher(path, key)){
                        replacer(path, params[key], t.valueToNode)
                    }
                })
            },
        
            Identifier(path, { opts: params }){// Match the identifier
                Object.keys(params).forEach(key= > {/ / traverse the Options
                    if(identifierMatcher(path, key)){
                        replacer(path, params[key], t.valueToNode)
                    }           
                })
            },
        }        
    }
}
Copy the code

Results 4.

Of course, this plugin cannot be configured in wepy.config.js. Because we have to execute our build script before wepy builds, replace the Pages field. So the solution is to run our compile script before running wepy Build –watch by introducing babel-core to convert the code

const babel = require('babel-core')
/ /... Omit the process of getting app.wpy, which will be covered later.
/ /... Omit the process of writing the visitor, and the syntax is slightly different from that of writing the plug-in.
const result = babel.transform(code, {
    parserOpts: {// Parser for Babel, configuration for Babylon. Remember to add classProperties, otherwise you won't be able to parse the class syntax of app.wpy
        sourceType: 'module'.plugins: ['classProperties']},plugins: [[{visitor: myVistor// Use our written visitors
        }, {
            __ROUTES__: pages// Replace with our Pages array}]],})Copy the code

Eventually, of course, the conversion was successful and the plugin was used in production. But this scheme was not subsequently used to replace the Pages field. For now, only the constants __ENV__: process.env.node_env and __VERSION__: version are replaced. Why is that? Since the identifier __ROUTES__ is converted to our routing table every time after compilation, should I manually delete it and add __ROUTES__ next time I want to replace it? I certainly will not do with our idea of automation engineering is incompatible with the matter. However, after writing this plug-in, we have learned a lot about how to find and replace our target nodes through the compiler.

Write the Babel script to recognize the Pages field

1. The train of thought

  1. First get the source code: app.wpy is a vUE like single file syntax. Js is in the script tag, so how to get this part of the code? Regular again? Oh, no, that’s too good. By readingWepy – source cli, the use ofxmldomThis library parses and retrieves the code inside the script tag.
  2. Write the visitor to traverse and replace the node: first, find the inheritance fromwepy.appClass, and then findconfigField, and the final match key ispagesObject of. Finally, the destination node is replaced
  3. After Babel is converted to code, it replaces the object code by reading and writing files. The great work is done! done!

Results 2.

Final script:

/** * @author zhazheng * @description precompile before wepy compilation. Get the Pages field in app.wpy and replace it with the generated routing table. * /
const babel = require('babel-core')
const t = require('babel-types')

//1. Import routes
const Strategies = require('.. /src/lib/routes-model')
const routes = Strategies.sortByWeight(require('.. /src/config/routes'))
const pages = routes.map(item= > item.page)

//2. Parse the js inside the script tag to get the code
const xmldom = require('xmldom')
const fs = require('fs')
const path = require('path')

const appFile = path.join(__dirname, '.. /src/app.wpy')
const fileContent = fs.readFileSync(appFile, { encoding: 'UTF-8' })
let xml = new xmldom.DOMParser().parseFromString(fileContent)

function getCodeFromScript(xml){
    let code = ' '
    Array.prototype.slice.call(xml.childNodes || []).forEach(child= > {
        if(child.nodeName === 'script') {Array.prototype.slice.call(child.childNodes || []).forEach(c= > {
                code += c.toString()
            })
        }
    })
    return code
}
const code = getCodeFromScript(xml)

// 3. Nested three-layer visitor to find nodes while traversing the AST tree
Wepy.app = wepy.app
const appClassVisitor = {
    Class: {
        enter(path, state) {
            const classDeclaration = path.get('superClass')
            if(classDeclaration.matchesPattern('wepy.app')){
                path.traverse(configVisitor, state)
            }
        }
    }
}
/ / 3.2. Find the config
const configVisitor = {
    ObjectExpression: {
        enter(path, state){
            const expr = path.parentPath.node
            if(expr.key && expr.key.name === 'config'){
                path.traverse(pagesVisitor, state)
            }
        }
    }
}
//3.3. Find pages and replace them
const pagesVisitor = {
    ObjectProperty: {
        enter(path, { opts }){
            const isPages = path.node.key.name === 'pages'
            if(isPages){
                path.node.value = t.valueToNode(opts.value)
            }
        }
    }
}

// 4. Convert and generate code
const result = babel.transform(code, {
    parserOpts: {
        sourceType: 'module'.plugins: ['classProperties']},plugins: [[{visitor: appClassVisitor
        }, {
            value: pages
        }],
    ],
})

// 5. Replace source code
fs.writeFileSync(appFile, fileContent.replace(code, result.code))
Copy the code

3. Usage

Simply execute this script before wepy Build –watch to automatically replace the routing table and automate the operation. Monitor file changes, add modules automatically re – run scripts, update routing table, first-class development experience ~

conclusion

The process of writing code in a more automated and engineered direction is rewarding. However, the script does have some shortcomings, at least the matching node part of the code is not rigorous. In addition, we interrupt an advertisement that our company’s Wind change Technology is recruiting front-end development:

  • Fresh year, one year experience, familiar with Vue front-end small fresh meat
  • Three years of experience in front of me! ! All!!! Want to! Want to! Not only does our dev team write great code, but male programmers have a 100% drop-out rate!! Come and join us!

Email address: [email protected]