爱收集资源网

春节放假了!风变的投稿分享

网络 2023-07-01 19:03

序言

你们开始元旦休假了吗?明日早读文章来自风变@渣正refn投稿分享。

@渣正refn,一枚成长中的后端,就职于风变科技,崇尚手动化是第一生产力

正文从这开始~

背景

近来公司举办了小程序的业务,派我去负责这一块的业务,其中须要处理的一个问题是接入我们web开发的传统构架—模块化开发。

我们来详尽说一下模块化开发具体是如何样的。

我们的git工作流采用的是gitflow。一个项目会拆分成几个模块,之后一人负责一个模块(对应gitflow的一个feature)独立开发。模块开发并与前端移动后再合并至develop进行集成测试,后续经过一系列测试再发布版本。

目录结构大体如图所示,一个模块包含了他自己的pages/components/assets/model/mixins/apis/routes/scss等等。

这些开发模式的用处不言而喻,每位人都可以并行开发,大大提高开发速率。此次就是要移植这些开发模式到小程序中。

目标

背景说完了,这么来明晰一下我们的目标。

我采用的是wepy框架,类vue句型的开发,开发体验十分棒。在vue中,一个组件就是单文件,包含了js、html、css。wepy采用vue的句型,但由与vue稍微有点区别,wepy的组件分为三种—wepy.app类,wepy.page类,wepy.component类。

对应到我们的目录结构中,每位模块实际上就是一系列的page组件。要组合这一系列的模块,这么很简单,我们要做的就是把这一系列page的路由扫描成一个路由表,之后插入到小程序的入口—app.json中。对应wepy框架那即是app.wpy中的pages数组。

扫描路由表

语句替换_不要用多说插件_修改语法神器

第一步!先得到所有pages的路由并综合成一个路由表!

我的方案是,在每位模块中新建一份routes文件,相当于注册每位须要插入到入口的page的路由,不须要接入业务的page就不用注册啦。是不是很熟悉呢,对的,就是参考vue-router的注册句型。

//routes.jsmodule.exports = [
   
{
       name
: 'home-detail',//TODO: name先占位,后续再尝试通过读name跳转某页
       page
: 'detail',//需要接入入口的page的文件名。例如这里是index.wpy。相对于src/的路径就是`modules/${moduleName}/pages/index`。
   
},
   
{
       name
: 'home-index',
       page
: 'index',
       meta
: {
           weight
: 100//这里加了一个小功能,因为小程序指定pages数组的第一项为首页,后续我会通过这个权重字段来给pages路由排序。权重越高位置越前。
       
}
   
}
]

而扫描各个模块并合并路由表的脚本十分简单,读写文件就ok了。

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) returnconst route = require(`${modulesPath}/${module}/route`)
   route
.forEach(item => {
       item
.page = `modules/${module}/pages/${item.page.match(/\/?(.*)/)[1]}`
   
})
   routes
= routes.concat(route)
})
fs
.writeFileSync(routeDest,`module.exports = ${JSON.stringify(routes)}`, e => {
   console
.log(e)
})

路由排序策略

const strategies = {
   sortByWeight
(routes) {
       routes
.sort((a, b) => {
           a
.meta = a.meta || {}
           b
.meta = b.meta || {}
           
const weightA = a.meta.weight || 0const weightB = b.meta.weight || 0return weightB - weightA
       
})
       
return routes
   
}
}

最后得出路由表

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

替换路由字段

Sofarsogood…问题来了,怎么替换入口文件中的路由字段。我如下做了几步尝试。

直接引入

我第一觉得就是,这不很简单吗?在wepy编译之前,先跑脚本得出路由表,再import这份路由表就得了。

import routes from'./routes'exportdefaultclassextendswepy.app{
 config
= {
   pages
: routes,//['modules/home/pages/index']window: {
     backgroundTextStyle
: 'light',
     navigationBarBackgroundColor
: '#fff',
     navigationBarTitleText
: '大家好我是渣渣辉',
     navigationBarTextStyle
: 'black'
   
}
 
}
//...
}

但是这样小程序肯定会炸啦,pages数组的值必须是静态的,在小程序运行之前就配置好,动态引入是不行的!不信的话诸位可以试试。这么就是说,划重点—-我们必须在wepy编译之前再预编译一次—-事先替换掉pages数组的值!

正则匹配替换

修改语法神器_不要用多说插件_语句替换

既然要事先替换,那就是要精准定位pages数组的值,之后再替换掉。难点在于假如精准定位pages数组的值呢?

最捞但是最快的方式:正则匹配。

事先定好编码规范,在pages数组的值的前后添加/*__ROUTES__*/的注释

脚本如下:

const fs = require('fs')
const path = require('path')
import routes from'./routes'functionreplace(source, arr) {
   
const matchResult = source.match(/\/\* __ROUTE__ \*\/([\s\S]*)\/\* __ROUTE__ \*\//)
   
if(!matchResult) {
       thrownewError
('必须包含/* __ROUTE__ */标记注释')
   
}
   
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)

app.wpy的变化如下:

//before export default class extends wepy.app{
 config
= {
   pages
: [
   
/* __ROUTE__ *//* __ROUTE__ */
   
],
   window
: {
     backgroundTextStyle
: 'light',
     navigationBarBackgroundColor
: '#fff',
     navigationBarTitleText
: '大家好我是渣渣辉',
     navigationBarTextStyle
: 'black'
   
}
 
}
//...
}
//after export default class extends wepy.app{
 config
= {
   pages
: [
/* __ROUTE__ */'modules/home/pages/index', /* __ROUTE__ */
   
],
   window
: {
     backgroundTextStyle
: 'light',
     navigationBarBackgroundColor
: '#fff',
     navigationBarTitleText
: '大家好我是渣渣辉',
     navigationBarTextStyle
: 'black'
   
}
 
}
//...
}

行吧,也终于跑通了。由于项目很赶,所以先用这个方案开发了一个半礼拜。开发完以后总认为这些方案太难过,于是合谋着换另一种各精准的手动的方案。。。

babel插件替换全局常量

1.思路

想必你们肯定很熟悉这些模式

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

通过这些只在编译过程中存在的全局常量,我们可以做好多值的匹配。

由于wepy早已预编译了一层,在框架内的业务代码是读取不了process.env.NODE_ENV的值。我就想着要不做一个类似于webpack的DefinePlugin的babel插件吧。具体的思路是babel编译过程中访问ast时匹配须要替换的标示符或则表达式,之后替换掉相应的值。诸如:

In

export default class extends wepy.app{
 config
= {
   pages
: __ROUTE__,
   window
: {
     backgroundTextStyle
: 'light',
     navigationBarBackgroundColor
: '#fff',
     navigationBarTitleText
: '大家好我是渣渣辉',
     navigationBarTextStyle
: 'black'
   
}
 
}
//...
}

Out

export default class extends wepy.app{
 config
= {
   pages
: [
       
'modules/home/pages/index',
   
],
   window
: {
     backgroundTextStyle
: 'light',
     navigationBarBackgroundColor
: '#fff',
     navigationBarTitleText
: '大家好我是渣渣辉',
     navigationBarTextStyle
: 'black'
   
}
 
}
//...
}

2.学习怎么编撰babel插件

在这儿先要给你们推荐几份学习资料:

首先是babel官网推荐的这份迷你编译器的代码,读完以后基本能理解编译器做的三件事:解析,转换,生成的过程了。

其次是编撰Babel插件入门指南。基本囊括了编撰插件的方方面面,不过因为babel几个工具文档的缺位,在写插件的时侯须要去翻查代码中的注释阅读api用法。

之后是大杀器AST转换器—astexplorer.net。我们来看一下,babel的解析器—babylon的文档,囊括的节点类型那么多,脑绘一张AST树不现实。我在编撰脚本的时侯会先把代码放到转换器内生成AST树,再一步一步走。

编撰babel插件之前先要理解具象句型树这个概念。编译器做的事可以总结为:解析,转换,生成。具体的概念解释去看入门指南可能会更好。这儿讲讲我自己的一些理解。

解析包括词法剖析与句型剖析。

解析过程吧。虽然按我的理解(不晓得这样合适不合适==)具象句型树跟DOM树似乎很类似。词法剖析有点像是把html解析成一个一个的dom节点的过程,句型剖析则有点像是将dom节点描述成dom树。

转换过程是编译器最复杂逻辑最集中的地方。首先要理解“树形遍历”与“访问者模式”两个概念。

“树形遍历”如指南中所举例子:

假定有那么一段代码:

functionsquare(n) {
 returnn
* n;
}

这么有如下的树状结构:

- FunctionDeclaration
 
- Identifier (id)
 
- Identifier (params[0])
 
- BlockStatement (body)
   
- ReturnStatement (body)
     
- BinaryExpression (argument)
       
- Identifier (left)
       
- Identifier (right)

语句替换_不要用多说插件_修改语法神器

“访问者模式”则可以理解为,步入一个节点时被调用的方式。比如有如下的访问者:

const idVisitor = {
 
Identifier() {//在进行树形遍历的过程中,节点为标识符时,访问者就会被调用
   console
.log("visit an Identifier")
 
}
}

结合树状遍历来看,就是说每位访问者有步入、退出两次机会来访问一个节点。

而我们这个替换常量的插件的关键之处就是在于,访问节点时,通过辨识节点为我们的目标,之后替换他的值!

3.动手写插件

话不多说,直接上代码。这儿要用到的一个工具是babel-types,拿来检测节点。

难度似乎并不大,主要工作在于熟悉怎样匹配目标节点。如匹配memberExpression时使用matchesPattern方式,匹配标示符则直接检测节点的name等等套路。最终成品及用法可以见我的github

const memberExpressionMatcher = (path, key) => path.matchesPattern(key)//复杂表达式的匹配条件const identifierMatcher = (path, key) => path.node.name === key//标识符的匹配条件const replacer = (path, value, valueToNode) => {//替换操作的工具函数
   path
.replaceWith(valueToNode(value))
   
if(path.parentPath.isBinaryExpression()){//转换父节点的二元表达式,如:var isProp = __ENV__ === 'production'   ===>   var isProp = trueconst result = path.parentPath.evaluate()
       
if(result.confident){
           path
.parentPath.replaceWith(valueToNode(result.value))
       
}
   
}
}
exportdefaultfunction
({ types: t }){//这里需要用上babel-types这个工具return {
       visitor
: {
           
MemberExpression(path, { opts: params }){//匹配复杂表达式Object.keys(params).forEach(key => {//遍历Optionsif(memberExpressionMatcher(path, key)){
                       replacer
(path, params[key], t.valueToNode)
                   
}
               
})
           
},
           
Identifier(path, { opts: params }){//匹配标识符Object.keys(params).forEach(key => {//遍历Optionsif(identifierMatcher(path, key)){
                       replacer
(path, params[key], t.valueToNode)
                   
}          
               
})
           
},
       
}        
   
}
}

4.结果

其实啦,这块插件不可以写在wepy.config.js中配置。由于必须在wepy编译之前执行我们的编译脚本,替换pages数组。所以的方案是在跑wepybuild--watch

之前跑我们的编译脚本,具体操作是引入babel-core来转换代码

const babel = require('babel-core')
//...省略获取app.wpy过程,待会会谈到。//...省略编写visitor过程,语法跟编写插件略有一点点不同。const result = babel.transform(code, {
   parserOpts
: {//babel的解析器,babylon的配置。记得加入classProperties,否则会无法解析app.wpy的类语法
       sourceType
: 'module',
       plugins
: ['classProperties']
   
},
   plugins
: [
       
[{
           visitor
: myVistor//使用我们写的访问者
       
}, {
           __ROUTES__
: pages//替换成我们的pages数组
       
}],
   
],
})

其实最终我们是转换成功啦,这个插件也用上了生产环境。并且后来没有采用这方案替换pages数组。暂时只替换了__ENV__:process.env.NODE_ENV与__VERSION__:version两个常量。

为何呢?

由于每次编译然后标示符__ROUTES__就会被转换成我们的路由表,这么上次我想替换的时侯莫非要自动删除之后再加上__ROUTES__吗?我其实不会干跟我们手动化工程化的思想命理不合的事情啦。

不过写完这个插件以后收获还是挺大的,基本了解该怎么通过编译器找寻并替换我们的目标节点了。

修改语法神器_不要用多说插件_语句替换

编撰babel脚本辨识pages数组

1.思路

首先获取到源代码:app.wpy是类vue单文件的句型。js都在script标签内,这么如何获取这部份代码呢?又正则?不好吧,太捞了。通过阅读wepy-cli的源码,使用xmldom这个库来解析,获取script标签内的代码。

编撰访问者遍历并替换节点:首先是找到承继自wepy.app的类,再找到config数组,最后匹配key为pages的对象的值。最后替换目标节点

babel转换为代码后,通过读写文件替换目标代码。大业已成!done!

2.成果

最终脚本:

/**
* @author zhazheng * @description 在wepy编译前预编译。获取app.wpy内的pages字段,并替换成已生成的路由表。 */
const babel = require('babel-core')
const t = require('babel-types')
//1.引入路由const Strategies = require('../src/lib/routes-model')
const routes = Strategies.sortByWeight(require('../src/config/routes'))
const pages = routes.map(item => item.page)
//2.解析script标签内的js,获取codeconst 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)
functiongetCodeFromScript
(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.在遍历ast树的过程中,嵌套三层visitor去寻找节点//3.1.找class,父类为wepy.appconst appClassVisitor = {
   
Class: {
       enter
(path, state) {
           
const classDeclaration = path.get('superClass')
           
if(classDeclaration.matchesPattern('wepy.app')){
               path
.traverse(configVisitor, state)
           
}
       
}
   
}
}
//3.2.找configconst configVisitor = {
   
ObjectExpression: {
       enter
(path, state){
           
const expr = path.parentPath.node
           
if(expr.key && expr.key.name === 'config'){
               path
.traverse(pagesVisitor, state)
           
}
       
}
   
}
}
//3.3.找pages,并替换const pagesVisitor = {
   
ObjectProperty: {
       enter
(path, { opts }){
           
const isPages = path.node.key.name === 'pages'if(isPages){
               path
.node.value = t.valueToNode(opts.value)
           
}
       
}
   
}
}
// 4.转换并生成codeconst result = babel.transform(code, {
   parserOpts
: {
       sourceType
: 'module',
       plugins
: ['classProperties']
   
},
   plugins
: [
       
[{
           visitor
: appClassVisitor
       
}, {
           value
: pages
       
}],
   
],
})
// 5.替换源代码
fs
.writeFileSync(appFile, fileContent.replace(code, result.code))

3.使用方式

只须要在执行wepybuild--watch之前先执行这份脚本,就可手动替换路由表,手动化操作。窃听文件变动,降低模块时手动重新跑脚本,更新路由表,开发体验一流~

结语

把代码往更手动化更工程化的方向写,这样的过程收获还是挺大的。并且确实这份脚本仍有不足之处,至少匹配节点这部份的代码是不大严谨的。

总结

话不多说先上图,简略说明一下干了些哪些事。图可能太模糊,可以点svg瞧瞧

最后,为你推荐

关于本文

不要用多说插件
上一篇:智能平推打印机助力高效工作 下一篇:没有了
相关文章