This paper introduces a hybrid development framework based on Vue that enables App to support offline caching of Web resources. I am small white one, please regard it as a summary of my study, welcome great gods to give advice. This article expounds more ideas, implementation details please read the source code.

The source code

Why hybrid development?

  • Efficient interface development: HTML + CSS + JavaScript have been proven to be extremely efficient interface development.

  • Cross-platform: unified browser kernel standard, so that H5 pages share the same code on IOS and Android. To develop a function using Native, one is required for IOS and Android, while one is enough for H5 front-end engineer. However, the less hybrid App is available, the better it will be. Those with higher performance requirements still need to work with Native… The division of labor should be clear, not favoritism.

  • Hot update: Updates applications independently of distribution channels. A new version of the Bug should be released on the Native fix line, and the Bug will always appear before the user upgrades the App. To fix H5, you only need to push the Fixbug code to the server, and any version of App can synchronize and update corresponding functions without upgrading.

Why cache Web Resources offline?

Instead of loading Web resources from a remote server, the App preferentially loads local preset resources, which improves page response speed and saves user traffic.

Here’s the problem… Local preset Web resources with the App installation package has become a splash out of water, fix H5 online Bug also need to issue version? Don’t throw away watermelon and pick up sesame! Please note “load local preset resources first”, but remote latest resources are loaded when updates are detected, how to detect updates I will clarify later.

Meaning to our front end team

  • Jinja + jQuery + Require + Gulp + Vue + Webpack + Gulp + Sass

  • Separation of front and back ends: The original Jinja is a Python template engine, and the operation of the front-end code depends on the server. The abnormal waiting of the server for environmental maintenance seriously affects the front-end work progress. After the separation, the Server hangs and we happily start the Mock Server and continue to move the bricks.

  • The App preferentially loads local preset Web resources, which improves the loading speed of H5 pages.

disadvantages

  • Technological refactoring is inherently risky.

  • Increase team learning costs.

  • Front-end frameworks rendering HTML through JS are not SEO friendly. However, you can choose to use server Rendering (SSR) in Vue 2.2. Add Node layer in addition to implement SSR, can do a lot of things…


Let’s get down to business

How the hybrid development framework works

The Web resource files are packed into dist/ (including routes. Json and n. do.html) and compressed into dist. Zip. The image resources are separately packed into assets/ and uploaded to CDN together.

Dist/All resources under App are preset in App (only download dist. Zip during release and decompress App during installation). After intercepting and parsing URL, search and load local.html page through routes.

Routes. Json as follows:

{
    "items": [{"remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Demo-13700fc663.html"."uri": "https://backend.igengmei.com/demo[/]?.*"
        },
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Album-a757d93443.html"."uri": "https://backend.igengmei.com/album[/]?.*"
        },
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/post/ArticleDetail-d5c43ffc46.html"."uri": "https://backend.igengmei.com/article/detail[/]?.*"}]."deploy_time": "Fri Mar 16 2018 15:27:57 GMT+0800 (CST)"
}
Copy the code

I owe you an answer

Please note “load local preset resources first”, but remote latest resources are loaded when updates are detected, how to detect updates I will clarify later.

The bridge for detecting.html file updates is routes.json. Every time App is started, routes. Json is silently updated from the CDN (CDN cache causes routes. Json cannot be updated in time, please add timestamp parameter to download routing table to force update).

Markup updates are made with a Hash (MD5) stamp on.html, which is a different file with different Hash suffixes for the App. App searches for the local.html based on the routing table remote_file. If the.html does not exist, it will directly load the remote resource and silently download the update.

Note: Since js and CSS scripts are inlined to the corresponding.html, App only needs to listen for changes in.html files. In fact, we can extract the common script and Hash it, recording the changes of the resource into a table for the App to listen to. Common scripts that are not updated all year round, cached in the App and not loaded with.html can also improve page response speed.

To sum up, Web resources are pre-installed in the App, but their Fixbug-level updates don’t have to go through the distribution route.

Why picture assets are packaged separately into assets/


Web Framework Design

Web framework design revolves around:

  • Reduce useless and redundant resources

  • Reduce the impact of dependent modules on Hash

  • The development environment pattern is as simple as possible

Reduce useless and redundant resources

You may have noticed that using Vue scaffolding builds results in monohtml, mono.js, and mono.css (all page resources bundled together in a single heap), but the example I used is poly.html. How to achieve Vue multi-page split I will talk in detail, first discuss the significance of split multi-page it: “fast” + “save”!

Suppose I contain pages A, B, and C, and the user accesses only A but the single-page application loads all the resources that A, B, and C depend on. B, C are useless to users, we secretly eat user traffic to download useless resources is not honest.

Splitting resources can reduce the size of.html and naturally improve page loading speed, and App preferentially access local.html without remote request is faster.

Unnecessary resources need to be discarded and public resources need to be extracted. Assuming that both pages A and B reference resource C, resource C can be extracted separately. The CommonsChunkPlugin can be used to remove third-party libraries and common components. An example of the node_module script used to extract the project:

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor'.minChunks: function (module) {
        return (
            module.resource &&
            /\.js$/.test(module.resource) &&
            module.resource.indexOf(
                path.join(__dirname, '.. /node_modules'= = =))0)}})Copy the code

The node_module applied to the project is packaged uniformly into vendor.js. Common scripts also need to be preset and checked for updates, or inline to.html if you think it’s a hassle to listen to a lot of resources, but I don’t recommend that (losing the sense of redundancy). Where do I copy the preset public scripts? Copy to the mobile phone memory space is not enough how to break, copy to the memory card is deleted by the user how to break, the client students are very entangled… emmm

Vendor.js contains the node_module that all pages depend on. Assuming that page A uses Swiper and no other page references it, swiper-related code in vendor.js should be packaged only to page A. How to implement this?

  • When generating vendor.js, filter Swiper and package it separately; node_modules still contains Swiper.

  • Move Swiper from node_modules to another path, referencing it using the migrated path.

The introduction of Sass can also remove useless code to a certain extent:

Generic styles defined using @mixin and % are not inherited and will not be parsed to produce CSS.

Sass: Syntactically Awesome Style Sheets for more.

Reduce the impact of dependent modules on Hash

Since your App needs to monitor listener.html changes and update resources in real time, you should pay special attention to the stability of Hash values and adhere to the principle of code modularity. Assuming that app.js and app.css are introduced globally, non-global code is not allowed to be added to these two files.

If module A is injected into app.js, its changes affect all.html Hash values, and pages that do not call module A actually update the Hash without making changes. The App determines resource updates based on changes in Hash and considers that all.html is updated, and then downloads all Web resources again.

In short, A does not call B, and the modification of B does not affect THE Hash of A. Please follow this principle to determine how to split modules.

Next, we discuss the injection timing of manifest. The Manifest contains module processing logic, module information is recorded in the Manifest when Webpack compiles and maps application code, and the Runtime loads modules according to the MANIFEST.

new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest'.minChunks: Infinity
})
Copy the code

Any module update causes its subtle changes (but the manifest scope can be controlled through minChunks), and all page loads depend on the MANIFEST. The manifest update all.html Hash update -> all.html is re-downloaded. We can Hash.html before inlining the manifest, since calling the old manifest without updating the module will not be affected.

The development environment pattern is as simple as possible

A project with a large number of participants and a complex development environment will increase the cost and risk of learning. What HAVE I done to simplify the development model:

Development environment single entrance, production environment multiple entrance

Let’s start with Vue multi-page split. A lot of relevant articles are recommended here, point me ~

Core ideas:

  • Single page: Multiple views correspond to single index. HTML + single entry.js.

  • Multiple pages: Multiple views correspond to multiple index. HTML + multiple entry.js.

If there are 100 views, create 100 index.html and 100 entry.js! But they are almost identical, and duplication is wasteful and increases development costs.

Index.html can be reused by multiple views, entry.js cannot. Shared Entry needs to import all views, so each page generated by build contains all the resources of each View, that is, 100 identical.html pages.

We can formally single entry, but actually multiple entry, how do we do that? Define an entry template with placeholders that are replaced by the entry of the corresponding View during build, so that import resources are split as needed.

Entry.js with placeholder <%=Page%> :

import Vue from 'vue'
import Page from '<%=Page%>'
/* eslint-disable no-new */
new Vue({
    el: '#app'.template: '<Page />'.components: {
        Page
    }
})
Copy the code

Generate gulp task with multiple entries:

gulp.task('entries', () = > {var flag = true
    for (let key in routes) {
        // Check whether entry already exists
        gulp.src(`./entry/entries/${routes[key].view}.js`)
            .on('data', () = > {// Entry already exists and does not duplicate the construction
                flag = false
            })
            .on('end', () = > {if (flag) {
                    console.log('new entry: '.`/entries/${routes[key].view}.js`)
                    // Construct a new entry
                    gulp.src('./entry/entry.js')
                        .pipe(replace({
                            patterns: [{match: /<%=Page%>/g.replacement: `.. /.. /src/views/${routes[key].path}${routes[key].view}`
                                }
                            ]
                        }))
                        .pipe(rename(`entries/${routes[key].view}.js`))
                        .pipe(gulp.dest('./entry/'))
                }
                flag = true})}})Copy the code

Only gulp entries can be executed in production environment to construct multiple entries, and only one entry can be executed in development environment, thus saving the cost of entry construction for r&d students.

function entries () {
    var entries = {}
    for (let key in routes) {
        entries[routes[key].view] = process.env.NODE_ENV === 'production'
            ? `./entry/entries/${routes[key].view}.js`
            : './entry/dev.js'
    }
    return entries
}
Copy the code
The development environment uses local images, and the production environment uses CDN images

Since the App only listens for.html changes, image resources need to be referenced remotely. It doesn’t seem complicated to upload pictures to CDN for research and development, but it is not allowed for our company to have excessive upload permissions on CDN.

The original communication cost is high, and waiting for others to upload also affects the efficiency of development.

Upload pictures to test CDN in development stage and copy them to online environment in production stage. Conversion costs are not small, and missing uploads can cause online accidents.

In the development stage, the relative path is written to refer to local resources, so as to avoid the trouble of uploading pictures by r&d itself, and the mode is consistent with traditional Web development. The production environment directly converts image links to CDN paths. All images are separately packaged to assets/ and uploaded to CDN together. At this time, the reference of.html to CDN images takes effect.

{
    test: /\.(png|jpe? g|gif|svg)(\? . *)? $/.loader: 'url-loader'.options: {
        limit: 1.name: 'assets/imgs/[name]-[hash:10].[ext]'}}Copy the code

Add the Hash suffix to the image name after build to prevent the image from being updated in time due to CDN cache. Here I set the Base64 conversion limit to 1 to prevent HTML interspersed with too many Base64 images from blocking loading.

The path code of image link conversion to CDN in production environment is as follows:

const settings = require('.. /settings')
module.exports = {
    dev: {
        // code...
    },
    build: {
        assetsRoot: path.resolve(__dirname, '.. /.. /dist'),
        assetsSubDirectory: 'static'.assetsPublicPath: `${settings.cdn}/ `.// code...}}Copy the code

Tool in

Html-webpack-inline-source-plugin, gulp-inline-source: JS, CSS resource inline tools.

Commons-chunk-plugin: common module splitter.

Gulp-rev, hashed-module-ids-plugin: MD5 signature generation tool.

Gulp-zip: compression tool.

Other common Gulp tools include gulp-rename, gulp-replace-task, and del


Notes on pit

Route Resolution Problem

Assume the route configuration is:

{
    "/demo": {
        "view": "Demo"."path": "demo/"."query": [
            "topic_id"."service_id"]},"/album": {
        "view": "Album"."path": "demo/"}}Copy the code

Json is generated as follows:

{
    "items": [{"remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Demo-2392a800be.html"."uri": "https://backend.igengmei.com/demo[/]?.*"
        },
        {
            "remote_file": "http://p2znmi5xx.bkt.clouddn.com/dist/demo/Album-1564b12a1c.html"."uri": "https://backend.igengmei.com/album[/]?.*"}]."deploy_time": "Mon Mar 19 2018 19:41:22 GMT+0800 (CST)"
}
Copy the code

Localhost :8080/demo? Topic_id =&service_id= Access the Demo page, which looks like the route vue-router built for us. In the production environment, the access path is file:////dist/demo/Demo-2392a800be.html? Uri = https://%3a%2F %2fBackend.igengmei.com %2Fdemo%3Ftopic_id%3D%26service_id%3D. To obtain parameters, resolve the URI.

$router = ‘this.$router. Query’;

const App = {
    $router: {
        query: (key) = > {
            var search = window.location.search
            var value = ' '
            var tmp = []
            if (search) {
                // The production environment parses urIs
                tmp = (process.env.NODE_ENV === 'production')?decodeURIComponent(search.split('uri=') [1]).split('? ') [1].split('&')
                    : search.slice(1).split('&')}for (let i in tmp) {
                if (key === tmp[i].split('=') [0]) {
                    value = tmp[i].split('=') [1]
                    break}}return value
        }
    }
}
Copy the code

$router can be bound to vue. prototype:

App.install = (Vue, options) = > {
    Vue.prototype.$router = App.$router
}
export default App
Copy the code

Execute in entry.js:

Vue.use(App)
Copy the code

Any. Vue can call this.$router directly without import. Bind to vue. prototype for more frequently called methods, such as this.$request.

Defect: The homemade router supports only query parameters but not param parameters.

Cookie synchronization problem

App loads local preset resources in file:/// field, so cookies cannot be directly loaded into Webview. Opening cookies to file:/// will cause security problems. Several solutions:

  • Distinguish file:/// from the source, determine the security of the source and load the Cookie, but H5 still cannot bring the Cookie to the request.

  • Forge similar HTTP requests to form fake domains.

  • Native maintains cookies and provides an interface to obtain them. H5 concatenates cookies and writes Request headers.

  • The Native generation sends the request and returns the returned value, but fails to implement the POST request with a large amount of data (for example, POST File).

CSRFToken is usually written into the Cookie when the page is render and sent back to the server when the Request is made to prevent cross-domain attacks. However, the above steps are missing when loading the local HTML, so you need to pay extra attention to obtaining the CSRFToken.

To be continued


Author: Silly love kitten

My garden: sunmengyuan. Making. IO/garden /

My Github: github.com/sunmengyuan

The original link: sunmengyuan. Making. IO/garden / 2018…