The introduction

The React, Vue, and AngularJS frameworks are so popular today that it’s hard to argue which is the best, but what they all have in common is the MVVM model. As you can see from a simple diagram, the MVVM model works best in the ViewModel layer. The ViewModel helps us get rid of cumbersome DOM manipulation and is a qualitative leap over the MVC pattern.

This article, however, is not about viewModels, but about models that are currently the most overlooked by front-end developers.

Model should be defined as the data layer in both MVC and MVVM patterns, and theoretically all data-related operations should be extracted to this layer. However, in my experience, front-end developers currently spend less energy on the Model layer, which may be due to the following reasons:

  • The data operation in the front-end engineering is relatively simple, mainly based on API calls, mainly using the data that has been basically processed by the back-end
  • Front-end business data processing is relatively simple, processing at the ViewModel level can meet the needs
  • Front-end developers generally lack experience in database operations and awareness of data management

Let’s take a look at the Model layer in a back-end language. Take the ThinkPhp framework popular in PHP circles as an example. Here’s a user Model:

Class User extends Model {public function getUserInfo($uid) {} class User extends Model {public function getUserInfo($uid) { Public function checkLockState($uid) {} public function checkLockState($uid) {} public function checkLockState($uid Public function getUserLikeList($uid) {} public function getUserLikeList($uid) {} public function getUserLikeList($uid) { getUserFriendList($uid) { } //...... Other operations}Copy the code

This code skips more methods and class inheritance and actually extracts all user-related add, delete, change, and query operations into a single data Model, using only the methods provided by the Model in the Control layer, instead of performing SQL queries in the Control layer.

Model requirements and solutions in front-end engineering

In fact, front-end engineering has many requirements for data extraction. Take a project (Vue project) that the author is in charge of for example, with the development of business, the code in the project expands rapidly, and different problems are faced in different stages. As the problems are gradually solved, a Model system is gradually formed. Next, we will look at it in detail according to the timeline of the question:

Requirement 1: Unified encapsulation of API requests

This requirement is very simple, we do not recommend directly using some class libraries to request, because this is completely detrimental to unified maintenance, for example:

//Bad case import axios from 'axios' axios.get('/user? ID=12345') .then(function (response) { //... }) .catch(function (error) { //... }); //Good case //main.js import axios from 'axios' Vue.prototype.$http = axios //demo.vue this.$http.get('/user? ID=12345') .then((response)=>{ //... }) .catch((error)=>{ //... });Copy the code

After encapsulating all requests into $HTTP, we can perform secondary encapsulation, some modification or data interception, etc., which will not be described here.

Requirement 2: Interface reuse

We often encounter situations where some interfaces are frequently reused by different pages, and the interface data needs to be processed every time the interface is used, resulting in code redundancy. For example:

//goods.vue this.$http.get('/getGoodsDetail? If (response && Response. respCode == 0 && Response. respData){// Extract the header from the interface This. HeadPic = response. RespData. Pics. The split (' | ') [0] / / the price from $points into this. NowPrice = response. RespData. Whether nowPrice / 100 / / mail this.isFreePostage = response.respData.goodsTag.freePostage //... Other data processing logic} else {/ / errors toast (response) errorMsg | | response. ErrMsg | | 'interface error')}}). The catch ((error) = > {/ /... });Copy the code

Getting product details is a requirement that many pages have, and each page needs to deal with the following issues:

  1. Determining the success of an interface request (business level)
  2. Error message handling of an interface request failure (different interfaces may return different error message fields)
  3. Unified processing of interface data: extraction of header diagram, subconversion, whether to include mail, etc
  4. Interface data extraction is not secure: Such as the response. RespData. GoodsTag freePostage this code has a high risk, because the interface is likely to be of no goodsTag fields lead to an error, the safety of the conventional reading strategies may be below the judgment for many times, can lead to increased code redundancy:
this.isFreePostage = response.respData.goodsTag && response.respData.goodsTag.freePostage
Copy the code

Based on the above problems, we may generally encapsulate the processing logic of commodity data into a method for reuse, such as:

const isRequestSuccess = (res)=>{ return res.respCode == 0 && res.respData } const getErrorMsg = (res)=>{ return Res. ErrorMsg | | res. ErrMsg | | 'interface error'} const convertDetailData = (res) = > {let data = res., respData return {headPic: data.pics.split('|')[0], nowPrice : data.nowPrice/100, isFreePostage : data.goodsTag && data.goodsTag.freePostage } }Copy the code

Such encapsulation does solve the immediate problem, but as complexity increases, the processing becomes more difficult to maintain; When there are other similar scenarios, such as user interfaces that require all of the above processing, the processing logic becomes more diverse, because you can’t predict what encapsulation other developers will do, and it costs more when you reuse other developers’ code.

So we start from here by extracting the data layer (Model).

The Model implemented

First of all, we have clear expectations:

  • The Model is uniformly structured
  • Models can be reused or inherited
  • All the data logic can be pre-processed in the Model, so developers can use the data directly without worrying about processing
  • The Model should have a clear logic of success and failure
  • The Model should provide secure logic for retrieving data

Let’s first post the code design for the Model:

//Model.js import Axios from '.. /libs/Axios' export default class Model {/** * constructor(options) {} /** * async fetch(options) {} /** * handleData(result) {} /** * handleData(result) {} /** * handleError(result) {} /** * reset request result */ resetResult() {}}Copy the code

Using class encapsulation, we define a base class that provides configuration, request, success or failure, data handling logic, error handling, and safe data reading methods. These methods can be inherited to implement personalized processing for different interfaces. Next we add the relevant code:

import Axios from '.. /libs/Axios' export default class Model {/** * constructor(options) {this.resetresult () this.domain = 'https://app.zhuanzhuan.com' enclosing gman.defaulttype = 'GET' this. The options = {} the if (options) enclosing the config (options)} / * * * data acquisition method  */ async fetch(options) { return new Promise(resolve => { Axios[this.options.type]( this.options.url, options ) .then(res => { this.resetResult() let result = res && res.data this.result.state = this.isSuccess(result) ? 'success' : 'fail' if (this.result.state == 'success') { this.result.data = this.handleData(result) resolve(this) } else { this.result.error = this.handleError(result) resolve(this) } }) .catch(res => { this.resetResult() this.result.error = {  errorCode: -9999, errorMsg: 'Network error'} resolve(this)})})} /** * config(options) {this.options = object.assign ({type: this.defaultType }, this.options, The options) if (this. Options. Type) enclosing the options. Type = this. The options. The toLowerCase () return this} / * * * * / judge success failure logic IsSuccess (result) {return parseInt(result.respcode) === 0 && result.respdata} /** ** handleData(result) { Return result.respData} /** * handleError(result) {return {errorCode: result.respCode, errorMsg: result.errorMsg || result.errMsg || (result.respData ? result.respData.errorMsg || result.respData.errMsg : ** * resetResult() {this.result = {state: null, data: null, error: null}}Copy the code

Above is the base class we defined. Now let’s see how developers can use it:

Canonical directory structure

First we need to standardize the Model directory structure:

├ ─ ─ project │ ├ ─ ─ the SRC │ │ └ ─ ─ the View / / template layer │ │ └ ─ ─ Model / / data layer │ │ │ └ ─ ─ Model. The js / / base class │ │ │ └ ─ ─ apiPath1 / / interface path │ │ │ │ ├ ─ getGoodsdetail.js // Model definition of the commodity detail interfaceCopy the code

The interface path is the basis for the classification of interfaces defined by us. The interfaces provided by different departments of our company are divided by apiPath, so we also use it as classification

Order Model

Now how does getGoodsdetail.js work

import Model from '.. /Model.js' class GetGoodsDetail extends Model { constructor(){ super() this.config({ url : '/apiPath1/getGoodsDetail', type: 'get'})} the handleData (result) {let data = result, respData extracting head figure data. / / from the interface headPic = data. The pics. The split (' | ') [0] / / the price by points converted into RMB Data. NowPrice = data. Whether nowPrice / 100 / / package mail data. The isFreePostage = data. GoodsTag &&. GoodsTag. FreePostage / / will handle good data back Return data} isSuccess(result){return parseInt(result.respCode) === 0 && result.respData // special logic: Think the goods is normal request in order to be successful && result. RespData. GoodsState = = 0}}Copy the code

When the page uses the Model:

//demo.vue import GetGoodsDetail from '@/model/apiPath1/GetGoodsDetail' export default { //..... methods : { async getGoodsDetail(){ let res = await (new GetGoodsDetail()).fetch({infoId:'123456'}) if(res.result.state == This. HeadPic = res.data.headpic this. NowPrice = res.data.nowprice this Res. Data. IsFreePostage / / deconstruction way to extract data let {headPic nowPrice, isFreePostage} = res. Data} else {/ / error message toast(res.result.error.errorMsg ) } } } }Copy the code

Requirement 3: Secure data extraction

In business development, the front-end and back-end usually agree on the interface data return format. For example, we agree:

{ respCode:0, respData:{ //.... GoodsTag: {//... FreePostage: 1}}Copy the code

We agreed that there must be goodsTag in the data, and whether the field freePostage is included or not, but the agreement is very good, and the reality is very fragile. You can’t trust that the server interface will definitely return you the data in the agreed format. Because of complex back-end architectures, complex data structures, and state, it is possible for unexpected formats to occur. For example, our error monitoring platform often catches this type of error:

This type of error is caused by data not returning according to the convention format, so if we want to ensure that the program does not report errors, we need to make a strict judgment:

let freePostage = res.respData && res.respData.goodsTag && res.respData.goodsTag.freePostage
Copy the code

If it’s too cumbersome to write, we need to add a method to the Model to get data safely:

//Model.js export default class Model { //... /** * get() {if (this.result && this.result.data) {return ((! Array.isArray(path) ? path .replace(/\[/g, '.') .replace(/\]/g, '') .split('.') : path ).reduce((o, k) => (o || {})[k], this.result.data) || defaultValue ) } else { return defaultValue } } }Copy the code

In this way, we can safely extract data according to path when we use it, for example:

//demo.vue import GetGoodsDetail from '@/model/apiPath1/GetGoodsDetail' export default { //..... methods : { async getGoodsDetail(){ let res = await (new GetGoodsDetail()).fetch({infoId:'123456'}) if(res.result.state == This.headpic = res.get('headPic') this.nowprice = res.get('nowPrice',0) this.isfreepostage = 1 Res. Get (' goodsTag. IsFreePostage, false) / / can even more complex data extraction enclosing firstGoodsCommentAuthor = Res. Get (' goodsComments [0]. The author ', 'anonymous')} else {/ / errors toast (res) result. Error. ErrorMsg)}}}}Copy the code

Requirement 4: A unified Model return format

Each Model returns the following data structure:

{ result : Data :null, // Processed data error:{// Error message errorCode: XXX, // Error code, -9999 indicates a network error errorMsg: XXX // Specific error information},}}Copy the code

Developers can rely on the result returned by Model when using it. For example, when determining the success of a request, developers do not need to consider the number of returned status code or the status of the product. When state == ‘success’ is returned, it can be considered as the successful status of the request in line with the business definition.

Requirement 5: Uniform error

The above Model can be further simplified. For example, if we want toasts for errors at the GetGoodsDetail Model level or on all interfaces, we can modify the Model class:

export default class Model { //.... /** * handleError(result) {let errorObj = {errorCode: result.respCode, errorMsg: result.errorMsg || result.errMsg || (result.respData ? result.respData.errorMsg || result.respData.errMsg : ErrorObj} // If you don't want to do this on all interfaces, Class GetGoodsDetail extends Model {/** * handleError(result) {// Same as above}}Copy the code

Requirement 6: Interface monitoring

We hope to do some monitoring of interface stability, so that we can find problems, and it is also convenient for us to do it in the Model layer. Of course, first of all, you need a system for error collection and display. Currently, popular open source systems include BadJS and Sentry, and our company is using Sentry. For example, we can encapsulate an error active reporting method:

Const captureException = (errType, errMsg, extendInfo) = > {} / / through the sentry wayCopy the code

Monitoring 1: Error monitoring

//Model.js export default class Model { //.... /** * handleError(result) {let errorObj = {errorCode: result.respCode, errorMsg: result.errorMsg || result.errMsg || (result.respData ? Result, respData errorMsg | | result. RespData. ErrMsg: ')} / / different type error reporting switch (errorObj. ErrorCode) {case 1: / / not login error captureException (' authFail 'errorObj errorMsg, enclosing the options) break; Case 2: / / other type error captureException (' otherError 'errorObj errorMsg, enclosing the options) break; Default: / / the default type error captureException (' apiError 'errorObj errorMsg, enclosing the options) break; } return errorObj } }Copy the code

Monitor 2: Monitors timeout

// model.js export default class Model {/** * constructor(options) {//... This.timeout = 10000} /** * data fetch method */ async FETCH (options) {return new Promise(resolve => {// record request start time let  startTime = (new Date()).getTime() Axios[this.options.type]( this.options.url, Options).then(res => {// Calculate the request time let fetchTime = (new Date()).getTime() - startTime // If (fetchTime >= This.timeout){// Send an error record captureException('apiTimeOut',fetchTime,this.options)} //..... }) / /... }}})Copy the code

Monitoring 3: Monitors network exceptions

Network exception errors are relatively easy to catch directly in the catch:

//Model.js export default class Model { //... /** * async fetch(options) {return new Promise(resolve => {Axios[this.options.type](this.options.url, options ) .then(res => { //... }) .catch(res => { this.resetResult() this.result.error = { errorCode: -9999, errorMsg: CaptureException ('netWorkError',res,this.options) resolve(this)})} //... }Copy the code

Requirement 7: Mock data management

We usually use mock data in the development process, which is usually another API address. For example, our company generally uses JSON-server. We expect to be able to store mock addresses in the Model for later testing or debugging, which can be modified further:

//GetGoodsDetail.js import Model from '.. /Model.js' class GetGoodsDetail extends Model { constructor(){ super() this.config({ url : '/apiPath1/getGoodsDetail', // We added a mockUrl field mockUrl to the child Model: 'http://localhost:3000/apiPath1/getGoodsDetail', type: 'get' }) } //.... } //demo.vue // export default {//... methods : {async getGoodsDetail(){// While debugging, we use mock methods to request let res = await (new getGoodsDetail()). Mock ({infoId:'123456'}) //..... }} / /... Export default class Model {//... / / way to increase a mock async mock (params) {/ / url to replace mockUrl enclosing the options. The url = this. Options. MockUrl | | this. The options. The url return this.fetch(params) } }Copy the code

Requirement 8: Easier to use Model– PROVIDE CLI support

Through the above changes, we have been able to use Model functions happily, but each interface needs to create a Model file, as the number of interfaces increases, it is a bit troublesome to use and find, so we can make a CLI tool to use, the effect is as follows:

With the zou Model interface name on the command line, the developer can automatically create/find the model (when it already exists) and print it out using the code

The functions are as follows:

/ / first of all, we create a new project / / package. The json {" name ":" zou - cli ", "bin" : {" zou ":" bin/cli "} / /... } // next we create a new /bin/cli file //cli #! /usr/bin/env node process.env.NODE_PATH = __dirname + '/.. /node_modules/' const { resolve } = require('path') const res = command => resolve(__dirname, '.. /commands/', command) const program = require('commander') program .version(require('.. /package').version) program.usage ('<command>') program.command ('model < API > [type]') // Define model command .description(' model').alias('m').action((API,type) => {let fn = require(res('model')) fn(API,type)}) program.parse(process.argv) if(! Js //model.js const {writeFile} = require('fs') const fse = require('fs-extra') function firstLetterToUpper(str) { return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase()); } let path = process.cwd() module.exports = (api,type)=>{ type = type || 'get' let apiInfo = api.split('/').splice(-3) if(! {apiInfo | | apiInfo. Length < 3). The console log (" \ [033 34 m API address is not standard, Check \033[0m") return} let className = firstLetterToUpper(apiInfo[2]) let apiPath = ` ${path} / SRC/model / ${apiInfo [1]} / ${apiInfo [2]}. Js ` fse. ReadFile (apiPath, function (err, file) {if! (err) {/ / file already exists Console. log("\033[34m model file already exists, please use \033[0m") console.log(' reference: The import ${className} from '@ / model / ${apiInfo [1]} / ${apiInfo [2]}. Js'; use: Let res = await (new ${className}()).fetch(); ') return}else {// File does not exist, Create fse.ensureFile(apiPath,function(err,exists){let modelFileContent = 'import Model from '@zz-vc/zz-open-libs/lib/model/Model' export default class ${className} extends Model { constructor(){ super() this.config({ url : '/${apiInfo[0]}/${apiInfo[1]}/${apiInfo[2]}', type: '${type}', }) } }` writeFile(apiPath, modelFileContent, 'utf-8', (err) => {if (err) {console.log(err) return} console.log('\033[34m created success!! \033[0m') console.log(' quote: The import ${className} from '@ / model / ${apiInfo [1]} / ${apiInfo [2]}. Js'; use: let res = await (new ${className}()).fetch() `) }) }) } }) }Copy the code

NPM run build && NPM publish is then published to NPM, and developers can use the command line after installing the package

Requirement 9: Interface caching

From a performance optimization perspective, we usually need some interface caching strategy, such as:

  • We expect to cache information such as national city data, commodity classification data and so on to the user’s browser, so that the second visit can directly use the cache to improve performance
  • We expect that the interface data of the first screen of important pages such as the home page can be cached, and the cached data will be displayed first during the second visit, and then new data will be requested from the server for comparison and display

Based on the above requirements and the application of Model, we can develop a matching interface caching function. We choose ES7 decorator as the function carrier. For easy understanding, let’s first look at the flow chart:

Specific code implementation:

// modelcache.js //Storage is a class library which can be used to specify the directory of the key in Storage and automatically manage the expiration time of data import Storage from '.. /storage' /** * interface cache decorator * @param {function} model incoming model instance * @param {number} expire cache expiration time * @param {Boolean} * @param {number} delay Specifies whether to request the server to update data after needUpdate hits the cache. */ function modelCache({model,expire = 7,needUpdate = true,delay = 0}){return function (target, funcName, function) */ function modelCache({model,expire = 7,needUpdate = true,delay = 0}) Descriptor) {let oriFunc = descriptor. Value, oriFetch = model.prototype. Fetch, CacheKey = 'modelCache-' + funcName, Model.prototype. fetch = async function(... args) { if(fetchState ! = 'usedCacheData' && cache){// If the state is not usedCacheData and there is a cache, Cache.get = model.prototype.get cache.dataType = 'cache' // to model Return cache}else {fetchState = 'usedFreshData' let res = await OriFetch. Apply (this,args) if(res.result.state == 'success'){oriFetch. Set (cacheKey,res,expire)} res.datatype = 'server' // Add a data source identifier to the model result return res}} // re-decorate the function descriptor.value = async function (... Args) {let res = await orifunc. apply(this, args); // If an update is needed, If (needUpdate && fetchState == 'usedCacheData'){setTimeout(()=>{orifunc. apply(this, oriforiforiforific.apply); Args. concat(true) // Add a parameter to the original function, which can be used to identify the second request},delay)} return res}}} export default modelCacheCopy the code

You can use the following in the page:

//demo.vue //... // We want to cache FindHomePage data with an expiration time of 15 days. @modelCache({model:FindHomePage, expire:15, needUpdate:true, delay:2000 }) async findHomePage(reExecution){ let res = await new FindHomePage().fetch() if(res.result.state == 'success'){if(reExecution){// This is the second execution of the method, If (res.datatype == 'cache') {if(res.datatype == 'server') {// Indicates that the data is from the server}} //... Other operations} //...Copy the code

Requirement 10: Multi-interface aggregation Model

While our Model was a single interface level encapsulation, we’re going to actually Model the data.

First of all, take the product details page of one of our projects as an example. Due to the technical architecture of the company, we need to read 9 interfaces to get all the required data, which are as follows:

  1. Basic commodity information interface
  2. Commodity auction information interface
  3. The auction information interface maintained by the business itself
  4. Merchant level interface
  5. Merchant information interface
  6. Commodity service label interface
  7. Recommended product list interface
  8. Commodity TWO-DIMENSIONAL code interface
  9. Goods can get red envelope interface

This is scary from a performance standpoint, and we’ll address it with GraphQL in later chapters.

Product data is a hell of an experience for developers. When a new page requires a certain amount of product data, it is difficult for developers to determine which interface to read to get the complete data. Therefore, we hope to provide developers with a convenient product model, which is expected to achieve the following results:

  1. Even if the data needs to come from multiple interfaces, there is no need to call the interfaces separately, and all the data can be retrieved in a single Model query
  2. Developers don’t care what interface the data comes from, just what data do I need
  3. You need to provide convenient, aggregated interface field query capabilities, preferably a visual interface
  4. You need to provide the ability to derive new data from data composition (more on that below)

Based on the above requirements, we first upgrade the Model design to achieve a multi-model aggregation capability

First, let’s define the directory structure

├ ─ ─ project │ ├ ─ ─ the SRC │ │ └ ─ ─ the View / / template layer │ │ └ ─ ─ Model / / data layer │ │ │ └ ─ ─ Model. The js / / base class │ │ │ └ ─ ─ mutiModel / / aggregation Model │ │ │ │ └ ─ ─ Goods/model/commodity │ │ │ │ │ └ ─ ─ config. Js / / configuration file │ │ │ │ │ └ ─ ─ index. The js files / / entryCopy the code

Next we write the configuration file

//config.js import Getoriinfodetail from '@/model/tobtoollogic/getOriInfoDetail.js'; import Getauctioninfobyid from '@/model/transfer/getAuctionInfoById.js'; import Checkmerchant from '@/model/tobtoollogic/checkMerchant.js'; import Getmerchantdetail from '@/model/tobtoollogic/getMerchantDetail.js'; import Getproductguaranteeinfo from '@/model/tobtoollogic/getProductGuaranteeInfo.js'; import Getrecommendinfolist from '@/model/tobtoollogic/getRecommendInfoList.js'; import Getwxmpcode from '@/model/tobtoollogic/getWxMpCode.js'; import Queryshopcouponwithreceivedinfo from '@/model/transfer/queryShopCouponWithReceivedInfo.js'; import GetOpenToBProductDetail from '@/model/tobtoollogic/getOpenToBProductDetail.js'; import cookie from '@/libs/cookie' export default { common : {basic: {name: 'basic information commodity, docId: 121899, connector: Getoriinfodetail}, auction: {name:' goods auction information, docId: 648, connector:Getauctioninfobyid }, auctionExtra : {name: 'business' own maintenance auction information, docId: 121913, connector: GetOpenToBProductDetail}, merchantStatus: {name: 'level of merchants', docId: 121833, Connector: Checkmerchant}, merchantInfo: {name: 'business information, docId: 121927, connector: Getmerchantdetail}, Service: {name: 'commodity service tag, docId: 121806, connector: Getproductguaranteeinfo}, how: {name:' recommended goods list, docId: 117528, Connector: Getrecommendinfolist}, qrCode: {name: 'commodity barcodes, docId: 117601, connector: Getwxmpcode}, RedPackage: {name: 'goods can get red envelopes, docId: 56895, connector: Queryshopcouponwithreceivedinfo}}, the custom: {settlemerchant :{name: "name", dependencies: "basic", compute:(res)=>{ return res.basic.get('searchable') == 52 } }, isMyGoods: < span style = "box-sizing: border-box; line-height: 21px; compute:(res)=>{ return res.basic.get('uid') && Cookies.getUID() && res.basic.get('uid') == Cookies.getUID() } }, isATGoods: =>{return res.basic.get('isYiGeProduct')}}, =>{return res.basic.get('isYiGeProduct')}}, isAuctionGoods: {name:' auctionExtra', Dependencies :'auctionExtra', compute (res)=>{return res.auctionextra. Get ('infoType') == 2}}}}Copy the code

There are two types of configuration files:

  • Common object: contains references to the 9 child models required by the product Model, where:
    • Key: alias
    • Value: indicates the configuration information
      • Name field: Model information
      • DocId field: id of the interface document
      • Connector: Child Model of the mapping
  • Custom object: Places custom combined data that is equivalent to new data computed through various data computations
    • Key: alias
    • Value: indicates the configuration information
      • Name: Data description
      • Dependencies: rely on
      • Compute: indicates the calculation method

Developers can create a variety of new fields based on the data, which opens up more possibilities for extending the Model

Let’s take a look at the entry file:

//index.js
import config from './config.js'
export default class Goods extends Model {
    constructor() {
      super()
      this.mutiModelConfig = config
    }
  }
Copy the code

Here we have a product model that we can use when we need product model data in a page:

//demo. Vue import Goods from '@/model/mutiModel/Goods/' let fields = [basic. IsPlanIdExists', "Basic. isYiGeProduct", "basic.followAmount", "basic.videoList", // All fields of the auction information, Auction.*", // custom field "isAuctionGoods"] // aggregate call method let res = await (new Goods()).want(fields).fetchall ({infoId: '123456'})Copy the code

As you can see from the above code, the developer only needs to configure which fields are required, and does not care which interface the fields are fetched from. In order to distinguish the normal Model call, we need to add two new methods:

  • Want: Configure required field functions
  • FetchAll: Request initiation function

So we’ll modify the Model base class again:

//Model.js export default class Model { constructor(options) { //.... This. MutiModelConfig = null; this. MutiModelConfig = null; */ want(fields = []) {if(typeof fields == 'string'){this.fields = [fields]}else If (array.isarray (fields)){this.fields = fields}else {console.warn('want method passed parameter type error, String array') return this} // Collect dependency Model if(this.mutiModelConfig && this.fields.length){// Default dependency let Mon defaultConnector = Object. Keys (this.mutiModelConfig.com) [0] / / collect fields inside explicitly depend on this. Dependencies = [] this.fields.forEach((v,k)=>{ if(v.indexOf('.') > -1){ this.dependencies.push(v.split('.')[0]) }else if(this.mutiModelConfig.custom && this.mutiModelConfig.custom[v]){ this.dependencies = this.dependencies.concat(this.mutiModelConfig.custom[v]['dependencies']) }else { Push (defaultConnector) this.fields[k] = defaultConnector + '.' + v // add a namespace to the default field}}) // Get all dependencies This. Dependencies = array. from(new Set(this.dependencies))} return this} /** * async fetchAll(options){ let Dependencies = this.dependencies.map(v=>this.mutiModelConfig.common[v]['connector']) return new Promise((resolve)=>{  Promise.all(dependencies.map(dependence=>new Dependence().fetch(options))) .then((res)=>{ let result = {}, Res.foreach ((v,k)=>{resObj[this.dependencies[k]] = v}) this.fields. ForEach (v=>{resObj[this.dependencies]] = v}) this.fields. if(v.indexOf('.') > -1){ let namespace = v.split('.')[0], key = v.split('.')[1], data = resObj[namespace] if(key == '*'){ result[namespace] = data }else { result[namespace] = result[namespace] || {} Result [namespace]. [key] = data get (key, null)}} else {/ / custom fields result [v] = this. MutiModelConfig. Custom [v]. Compute (resObj)  } }) resolve(result) }) .catch((err)=>{ console.error(err) resolve(null) }) }) } }Copy the code

Requirement 11: Visualization of the aggregate Model

Aggregation of Model requests is easy, but finding the required fields is still a hassle, so let’s implement a visual query interface again. Here we use a local service to run the query interface through the command line tool described above

First add a command:

//bin/cli program.command (' modelView ').description(' open modelview').alias('mv').action(() => {let fn = require(res('modelview')) fn() })Copy the code

Then write the command code:

//commands/modelview.js #! /usr/bin/env node const fs = require('fs') const http = require('http'); const request = require('request'); const opn = require('opn') let path = process.cwd() module.exports = ()=>{ let target = {} let dirName = ForEach ((filedir)=>{${path}/ SRC /model/mutiModel 'let files = fs.readdirsync (dirName) Let subDir = dirName + '/' +filedir let stats = fs.statsync (subDir) if(stats.isdirectory ()){// Read config file let ConfigFile = subDir + '/config.js' let file = fs.readfilesync (configFile) // Read all models and the corresponding docId let fileContent = in regular mode  file.toString(), reg = new RegExp(`(\\w+)\\W*\\:\\W*\\{[^{]*docId[^\\d]+(\\d+)`,'g'), result = null target[filedir] = {} while((result = reg.exec(fileContent)) ! = null){if(result && result.length && result.length > 2){target[filedir][result[1]] = result[2]}}}}) // Read the visual page Let htmlFile = fs.readfilesync (__dirname + '/ modelView.html ') // Write the preset variables of the configuration read to the template let htmlContent = htmlFile.toString().replace(`<script id="model"></script>`,`<script id="model"> let config = ${json.stringify (target, ",2)} let apiData = APIDATAPLACEHOLDER </script> ')/const getDoc = (id)=>{${json.stringify (target, ",2)} let apiData = APIDATAPLACEHOLDER </script> ') Return new Promise((resolve)=>{request(' http://api address /docId=${id} ', {json: true }, (err, res, body) => { if (err) { console.log(err) resolve({}); }else { resolve(body) } }); })} /** */ const createServer = (htmlContent)=>{const hostname = '127.0.0.1'; const port = 9433; Const server = http.createserver ((req, res) => {res.writehead (200, {' content-type ': 'text/ HTML '}); // Create a server const server = http.createserver ((req, res) => {res.writehead (200, {' content-type' : 'text/ HTML '}); res.write(htmlContent); res.end(' '); }); Listen (port, hostname, () => {console.log(' server run on: http://${hostname}:${port}/ '); // Enable server.listen(port, hostname, () => {console.log(' server run on: http://${hostname}:${port}/ '); opn(`http://${hostname}:${port}/`) }); } let apiList = [] for(let I in target){for(let item in target[I]){let apiList = [] for(let I in target[I]){ Apilist.push ([I +'.'+item,target[I][item]])}} // Read all API contents and start service Promise.all(apiList.map(v=>getDoc(v[1]))).then((res)=>{ let result = {} res.forEach(v=>{ if(v.result && v.result.docId){ Result [v.result.docId] = v}}) // Replace the preset variable with the interface content htmlContent = Htmlcontent.replace ('APIDATAPLACEHOLDER', json.stringify (result,null,2)) // Enable service createServer(htmlContent) }).catch((err)=>{ console.error(res) }) }Copy the code

When the user executes zou ModelView from the command line, a visual query interface opens in the browser

Preview the effect:

Part of the HTML template code in the above interface will not be pasted here, it is all data display and DOM manipulation

Requirement 12: Model interfacing with GraphQL

Those with performance optimization may have doubts about this. This kind of aggregation has no improvement in performance, but slightly decreases because of promise.all. Yes, it is true from the perspective of performance, and the biggest effect of the above method is to extract complex business data model highly. Save the cost for developers to pay attention to the source of field data. If you are qualified, you can interconnect with GraphQL on this basis to solve the performance problem perfectly

There are a lot of articles on the web about how to build the server part of GraphQL and how to write the schema.

First modify config and add a field

//config.js export default {gqlUri: 'https:// Your GraphQL request address', common: {//... }, custom : {//... }}Copy the code

Modify the Model base class again

//Model.js import ApolloClient from 'apollo-boost'; Export default class Model {/** * aggregation Model data collection method */ async fetchAll(options){let Dependencies = This. The dependencies. The map (v = > this.mutiModelConfig.com mon [v] [' connector ']) / / logic will result extracted const dataHandler = (res) = > {let result = {}, Res.foreach ((v,k)=>{resObj[this.dependencies[k]] = v}) this.fields. ForEach (v=>{resObj[this.dependencies]] = v}) this.fields. if(v.indexOf('.') > -1){ let namespace = v.split('.')[0], key = v.split('.')[1], data = resObj[namespace] if(key == '*'){ result[namespace] = data }else { result[namespace] = result[namespace] || {} Result [namespace]. [key] = data get (key, null)}} else {/ / custom fields result [v] = this. MutiModelConfig. Custom [v]. Compute (resObj) }}) return result} return new Promise ((resolve) = > {the if (this. MutiModelConfig. GqlUri) {/ / GraphQL request / / initializes a Apollo instance const client = new ApolloClient({ uri: this.mutiModelConfig.gqlUri }); Let gqlQuery = this._translategQLQuery (options,this.fields) Client.query (gqlQuery).then((res)=>{res = this._formatQQlResult (res) let result = dataHandler(res) Resolve (result)}). Catch ((err)=>{console.error(err) resolve(null)})}else {//RESTful request Promise.all(dependencies.map(dependence=>new Dependence().fetch(options))) .then((res)=>{ let result = dataHandler(res) resolve(result) }) .catch((err)=>{ console.error(err) resolve(null) }) } }) } }Copy the code

Our company has encapsulated a special client library for GraphQL, which involves complex logic, so the above code has been simplified to a certain extent. Apollo-boost is taken as an example of request mode, and students who are interested can further study it

Requirement 13: Model combined with WebSQL to achieve front-end data management

In business development, we often need some unchanged structured data, such as national city data and commodity classification data, which are usually used in two ways:

  1. The disadvantage of this method is that each time we need to request data, the data volume is large, and we may need some data format conversion operations every time, resulting in performance waste
  2. After API reading, cache to localStorage, this way has a certain improvement, to avoid every request, but localStorage can only be stored in text form, each use requires the string to JSON process, in the case of large amount of data, the code execution time is long, And it’s likely to cause gridlock

Based on the above considerations, we can try to store this kind of data in the WebSQL of the browser and use it as SQL. Example:

//Model.js import Database from './Database.js' export default class Model { //.... / create / * * * * / connect to the Database method async connect (params) {return new Promise ((resolve) = > {this. Db = this. Db | | new Database () This.db.isexists (this.options.url). Then (res=>{// Database already exists, resolve({result: {state:'success', data:this.db}})}). Catch (e=>{state:'success', data:this.db}}). Let res = await this.fetch(params) if(res.result.state == 'success'){for(let I in res.result.data){// create table and store data this.db.create(i,data[i]) } resolve({ result : { state:'success', data:this.db } }) }else { resolve(res) } }) }) } }Copy the code

Page call method:

//demo.vue import Category from '@/model/apiPath1/Category.js' export default { //..... methods : { async getCategory(){ let res = await (new Category()).connect() if(res.result.state == 'success'){ let db = Res.result. data // Select method parameters corresponding to SQL statements: FirstCategory = db. Select ('category',{level:1},'*','cateId ' Desc ', 'a', ' ')} else {/ / errors toast (res) result. Error. ErrorMsg)}}}}Copy the code

As for WebSQL related introduction and operation methods, I will cover them in detail in another article because the content is not relevant to this article

conclusion

So far, we have realized all the ideas of separating the Model layer. This set of wheels has been used in many projects of our company, which can effectively separate data from templates and logic.

JavaScript is a relatively loose language. With the development of recent years, JavaScript is constantly improving and absorbing the advantages of other languages. TypeScript is a good example.