Scaffolding includes features like Egg, Webpack, ESLint, Babel, Happypack, Sass, Vue, Lint-Staged, hot update, and more. Provides configurability of WebPack build, flexible extension, easy to use.

What are the main problems that scaffolding solves

Egg is an excellent enterprise node framework. It is commonly used in the following scenarios: 1. Used to do BFF layer, 2. Used to do full stack application, 3. Do server-side rendering page, SEO optimization. Theoretically, it belongs to the development of the server side, and the code of the browser side still needs a mechanism to organize, so that we can better integrate the development of the front and back end. For example, HTML, CSS and JS are unrelated to egg, so we can use Webpack to package its modules. After using it for a period of time, some problems are summarized as follows:

  • How the egg’s front-end code is developed locally;
  • How the front-end code is hot updated;
  • If we are doing some pages that do not need SEO optimization, such as complex form pages, personal user centers, etc., if we can introduce an MVVM framework at this time, it will greatly improve our development efficiency. Our first choice is VUE, because it is relatively lightweight and easy to use.

The starting point of the idea is to solve these main problems, of course, there will be some details of the problem, when solved one by one, we will basically implement the scaffolding.

Github: egg-multiple-page-example welcomes star

The directory structure

Take a look at the scaffold’s directory structure, which is easier to understand with annotations.

Egg - multiple - page - example | ├ ─ app. Js egg startup files, you can do something at the time of application to start | │ ├ ─ app project directory, main storage node side code, with regular egg directory structure, │ │ ├─ Controller │ │ detail.js ├─ ├─ example │ │ ├.js │ ├.js │ ├.js │ ├.js │ │ ├─ Extend extension module │ │ application.js │ │ context.js │ │ helper.js │ │ ├─extend module │ │ home.js │ │ vue Request.js │ │ ├─ Middleware Module │ │ errorHandle.js │ │ ├─ Middleware Module │ │ ErrorHandle.js │ │ ├─ Middleware module │ │ ├ ─ imp, ├ ─ imp, ├ ─ imp, ├ ─ imp, ├ ─ imp, ├ ─ imp, ├ ─ imp, │ ├─build. Js │ ├─build. Js │ ├.js │ config.js │ │ devserver.js │ │ utils.js │ │ webpack.base.conf.js │ │ webpack.dev.conf.js │ │ devserver.js │ │ hotReload Webpack. PRD. Conf. Js │ │ │ ├ ─ loaders custom webpack loaders │ │ hot - reload - loader. Js │ │ │ └ ─ plugins custom webpack plugins │ │ ├─ Config Egg, compile-html-plugin.js │ ├─ Config Egg, Js │ config.dev. Js │ config.local.js │ config.prod.js │ config.test.js │ │ ├─ ├─ SRC ├─assets ├─ ├─ exercises, ├─ exercises, exercises, exercises, exercises, exercises, exercises, exercises Some doc, Excel, sample pictures, etc., will be copied to the Dist /static directory ├─common public modules, such as CSS and JS. │ ├─ CSS public style │ │ common.scss │ │ ├─ js public js │ │ initrun.js │ │ ├─ Images ├─ favicon.ico │ │ ├─ Common Public Images │ │ ├─ Favicon. The image below catalog will not into base64, also won't add the md5, used for reusable image and external image │ └ ─ example modules the image below, small image turns base64 │ vue - logo. PNG │ └ ─ code templates business directory, For each page and the code of the component, the components to retain directory ├ ─ the components directory of custom components, vue components in vue directory │ ├ ─ footer if the component, including HTML, js, CSS must use directory wrapped up, │ │ ├─ Header │ │ ├─ If the component is just HTML, you can use it as an HTML file. │ │ header. HTML │ │ ├ ─ trash ├ ─ 0 ├ helloWorld. ├ ─example ├─detail a directory and a page, including HTML, CSS, JS files, │ ├─home │ home. HTML │ detail.js │ detail. SCSS │ ├─home │ home.js │ home. SCSS │ ├─ ├─ vue app.vue vue vue.js vue.scssCopy the code

Front and back end code interaction diagram

The webPack process is used to compile HTML, CSS, js, etc. The HTML will be written to a local temp directory, so that the EGG can read the HTML template directly. The CSS and JS will be mounted to the Express server so that we can access the CSS and JS code over HTTP. One problem with this is that the egg and Express servers have different ports, and the actual page we visit is on the Egg. The express server is used to provide CSS and JS, and the page is loaded with CSS and JS using a relative path, not the path on the Express server. It will also cause hot updates to fail because it is cross-domain.

In this case, we can use Nginx as a reverse proxy, using the same Nginx as the primary server, and then using Nginx to proxy egg and Express, bringing the two servers together and solving the cross-domain problem. To solve problem 1 above, here is the configuration of nginx:

server { listen 80; server_name local.example.com; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; Proxy_pass http://127.0.0.1:7113/; Use} # development environment, production environment need to note the location/static / {proxy_pass http://127.0.0.1:7213/static/; }}Copy the code

Core code explanation

If you are familiar with react or Vue development, it will automatically notify the browser to update the module code after the JS or CSS code is modified. The whole development process is very smooth. But what about updates on multiple pages? In webpack, hot update is through HotModuleReplacementPlugin and module. The hot, just can achieve the effect of hot update accept method combining. The simplest way to do this is to add the following code to the entry file:

if (module.hot) {  
  module.hot.accept();
}
Copy the code

Webpack notifies the browser when the module or its own module code is updated. Add this code to the main JS of each page. But isn’t it a little silly, if there are 50 pages with 50 pieces of code like this… orz Here I think of a way to use the custom loader, every JS compile time automatically add this code is not ok.

// hot-reload-loader.js
module.exports = function (source) {
  // Add hot update code after the js source code
  let result = source + ` if (module.hot) { module.hot.accept(); } `;
  return result;
};
Copy the code
// webpack.base.conf.js
// Add HMR code to js. {test: /\.js$/.loaders: devMode && reload ? [].concat(['hot-reload-loader'] : [],include: [path.join(__dirname, '.. /src/templates')]},...Copy the code

At this time, I encountered another problem. At first, I used the htmlWebpackPlugin plug-in to compile HTML, mainly to automatically inject CSS and JS into HTML. When more and more pages are available, the COMPILATION of HTML will be slower and slower. Changes to one page trigger compilation of all pages, no wonder it’s so slow. After searching the Internet for a long time and failing to find a solution, well, do it yourself. The first thing we need to do is inject CSS and JS, and make changes to one page without triggering a recompilation of all pages. By treating THE HTML as an entry file (like a JS file), we can have WebPack listen to the HTML file.

// utils.js
/** * Initializes the entry file * @param globPath Traversal file path * @returns {{}} webpack Entry object */
  initEntries (globPath) {
    let files = glob.sync(globPath);
    let entries = {};
    files.forEach(function (file) {
      let ext = path.extname(file);
      /* Just get the templates directory */
      let entryKey = file.split('/templates/') [1].split('. ') [0];
      if (ext === '.js') {
        /* Components do not need to add initrun.js */
        if(! (file.includes('/templates/components/'))) {
          entries[entryKey] = ['./src/common/js/initRun.js', file];
        } else{ entries[entryKey] = file; }}else{ entries[entryKey + ext] = file; }});return entries;
  }
Copy the code

Then in webpack, all HTML and JS files are as entry files:

// webpack.base.conf.js
const webpackConfig = {
  entry: utils.initEntries('./src/templates/**/*.{js,html,nj}}'),
  output: {
    path: outputPath,
    filename: 'js/[name].js'.publicPath: publicPath
  },
  ...
Copy the code

So HTML is treated by Webpack as a JS file, and after my research, whenever the JS file executes itself, it will return a string of HTML code, and the images and static resources in it will automatically compile to the correct path (or base64). Html-loader compiles tags such as IMG in HTML.

The next step is to figure out how to insert CSS and JS tags. We can use the WebPack compiler and the compilation hooks function to insert CSS and JS into the HTML module after it has been compiled. To do this, I made a Webpack plugin:

// compile-html-plugin.js
/** * Custom webpack plugin for optimizing multi-page HTML compilation. HtmlWebpackPlugin In the case of multiple pages, a change to one page triggers the compilation of all pages (dev), which becomes very slow once the project has more than a certain number of pages (dozens). * Replacing htmlWebpackPlugin with this plugin will not trigger all pages to be compiled, only the page you are currently modifying will be compiled, so it is very fast and written to the temp directory. * The plugin uses the events and methods of the custom WebPack Plugin, as described in the documentation:  * https://doc.webpack-china.org/api/plugins/compiler * https://doc.webpack-china.org/api/plugins/compilation */

'use strict';
const vm = require('vm');
const fs = require('fs');
const _ = require('lodash');
const mkdirp = require('mkdirp');
const config = require('.. /config');

class CompileHtmlPlugin {
  constructor (options) {
    this.options = options || {};
  }
  // Define 'apply' as its prototype method, which takes compiler as an argument
  apply (compiler) {
    const self = this;
    self.isInit = false; // Whether the initial compilation has been initialized
    self.rawRequest = null; // Record the current HTML path, which is used for compiling HTML once

    /** * WebPack4 plugin adds the compilation hook method to attach to the CompileHtmlPlugin */
    compiler.hooks.compilation.tap('CompileHtmlPlugin', (compilation) => {
      /* This method is the only one that can listen for a single file compilation */
      compilation.hooks.succeedModule.tap('CompileHtmlPlugin'.function (module) {
        /* module.rawRequest gets the path to the current module, and only HTML and nj files are compiled */
        if (self.isInit && module.rawRequest && /^\.\/src\/templates(.+)\.(html|nj)$/g.test(module.rawRequest)) {
          console.log('build module');
          self.rawRequest = module.rawRequest; }}); });/** * After compiling, before sending the resource to the output directory */
    compiler.hooks.emit.tapAsync('CompileHtmlPlugin', (compilation, cb) => {
      /* Webpack first executes */
      if(! self.isInit) {/* Iterate through all entry files */
        _.each(compilation.assets, function (asset, key) {
          if (/\.(html|nj)\.js$/.test(key)) {
            const filePath = key.replace('.js'.' ').replace('js/'.'temp/');
            const dirname = filePath.substr(0, filePath.lastIndexOf('/'));
            const source = asset.source();

            self.compileCode(compilation, source).then(function (result) { self.insertAssetsAndWriteFiles(key, result, dirname, filePath); }); }});/* Execute */ to modify the HTML once
      } else {
        /* rawRequest is not empty, indicating that the modification is HTML, can be compiled */
        if (self.rawRequest) {
          const assetKey = self.rawRequest.replace('./src/templates'.'js') + '.js';
          console.log(assetKey);
          const filePath = assetKey.replace('.js'.' ').replace('js/'.'temp/');
          const dirname = filePath.substr(0, filePath.lastIndexOf('/'));
          /* Get the current entry */
          const source = compilation.assets[assetKey].source();

          self.compileCode(compilation, source).then(function (result) {
            self.insertAssetsAndWriteFiles(assetKey, result, dirname, filePath, true);
          });
        }
      }

      cb();
    });

    /** * Complete compilation, do some property reset */
    compiler.hooks.done.tap('CompileHtmlPlugin', (compilation) => {
      if(! self.isInit) { self.isInit =true;
      }
      self.rawRequest = null;
    });
  }

  /** * use the vm module to sandbox the required *.html.js and get the HTML string returned after the run. * @param Compilation webpack compilation object * @param source code * @returns {*} */
  compileCode (compilation, source) {
    if(! source) {return Promise.reject(new Error('Please enter source'));
    }

    /* Defines the running context of the VM, that is, some global variables */
    const vmContext = vm.createContext(_.extend({ require: require }, global));
    const vmScript = new vm.Script(source, {});
    // The compiled code
    let newSource;
    try {
      /* newSouce is the result of sandboxed js execution, which is used to get the compiled HTML string */
      newSource = vmScript.runInContext(vmContext);
      return Promise.resolve(newSource);
    } catch (e) {
      console.log('-------------compileCode error', e);
      return Promise.reject(e); }}/** * Insert js and CSS into the HTML template, * @param assetKey Key of the current HTML in the entry object * @param result Template string of HTML * @param dirName Directory written * @param filePath Write file path * @param isReload Whether you need to tell the browser to refresh the page if hotMiddleware */ is passed in to use the plug-in
  insertAssetsAndWriteFiles (assetKey, result, dirname, filePath, isReload) {
    let self = this;
    let styleTag = `<link href="${config.publicPath}css/${assetKey.replace('.html.js'.'.css').replace('js/'.' ')}" rel="stylesheet" />`;
    let scriptTag = `<script src="${config.publicPath}${assetKey.replace('.html.js'.'.js')}"></script>`;

    result = result.replace('</head>'.`${styleTag}</head>`);
    result = result.replace('</body>'.`${scriptTag}</body>`);

    mkdirp(dirname, function (err) {
      if (err) {
        console.error(err);
      } else {
        fs.writeFile(filePath, result, function (err) {
          if (err) {
            console.error(err);
          }

          // Notify the browser of updates
          if (isReload) {
            self.options.hotMiddleware && self.options.hotMiddleware.publish({ action: 'reload'}); }}); }}); }}module.exports = CompileHtmlPlugin;
Copy the code

The code is not complicated, but the key points are:

  1. Nodejs vm module is used to create an independent running sandbox for self-executing compilation of HTML JS code;
  2. After compiling, it needs to be inserted into the head and body tags<link>and<script>The label;
  3. Write the HTML code after inserting the tag to the local directory;

This solves the HTML compilation problem.

So let’s do problem number three. Question 3 is not very difficult, the key is to analyze our requirements, we actually need vUE data driven, data binding and component functions, the upper tools, such as VUe-Router, VUex, VUE-CLI are not necessary. These are mainly used for vUE single page applications or SSR. Fortunately, Vue is an incremental framework, so we can simply introduce vue.js.

VueLoaderPlugin and VUUE-loader can be used as follows:

// webpack.base.conf.js. const VueLoaderPlugin =require('vue-loader/lib/plugin'); . module: {rules: [
      // Use vue-loader to compile vue files into JS
      {
        test: /\.vue$/.loader: 'vue-loader'}},]... plugins: [new VueLoaderPlugin(),
    ...
]
Copy the code

As simple as that, we introduced VUE into our project. Not all VUE projects require VUE-CLI. Using VUE in a project, we can also use some tricks to improve our page loading speed, such as lazy loading. Here are some examples of loading methods:

// Traditional synchronous loading
import Vue from 'vue';
import app from './app.vue';
new Vue({
  el: '#app'.render: h= > h(app)
});

// Load js asynchronously in sequence
import('vue').then(async ({ default: Vue }) => {
  const { default: app } = await import('./app.vue');
  new Vue({
    el: '#app'.render: h= > h(app)
  });
});

// Multiple asynchronous JS loads simultaneously
Promise.all([
  // Add names to asynchronous js when packaging
  import(/* webpackChunkName: 'async' */ 'vue'),
  import('./app.vue')
]).then(([{ default: Vue }, { default: app }]) = > {
  new Vue({
    el: '#app'.render: h= > h(app)
  });
});
Copy the code

Another thing to note is that the root node you mount in HTML must have the same ID as the root node of vue (of course, you can use class), such as #app, otherwise hot update will not find the mount element, error.

Some webPack performance optimizations and common code extraction, such as Happypack, OptimizeCSSPlugin, splitChunks, etc., are also used in the project. All of these are available in official documentation and will not be explained here.

The last

If you don’t understand something, you can leave a comment below or make an issue on Github. If the project is helpful to you, please give me a star. portal