preface

Github full project address

I haven’t written an article for a long time. I wrote an article about WebPack4 + EJS + Express to help you develop a multi-page application project Architecture. After it was published, I found that there was a strong demand for simplifying the multi-page application development process. As I applied the multi-page development model introduced in the previous article to real projects, I found that there were still some drawbacks. One pain point is the use of Express as a backend development framework.

Express is good, but today’s development is about efficiency, so-called hand out of the box, such a contrast, Express is still more low-level, as a server framework, a lot of things still have to bother to find the plug-in install to read the documentation. So, this time I’m going to use a higher-level egg as an alternative framework.

Although it is the evolution version of the last version, but many of the main implementation ideas are unchanged, want to know more about the friend recommended first look at the last article “[actual] WebPack4 + EJS + Express take you to a multi-page application project architecture”, this article only do the key step analysis, Detailed code can be seen at Github complete project address

The project structure

The most important directories have been highlighted in the screenshot. We can expand to see the detailed directory structure of the main directory:

An egg layer

After the introduction of the project structure, the following is about to begin to transform the previous code, but so much code from where to start? Our main goal this time was to replace Express with Egg, which of course started with Egg.

Before the transformation, we also need to understand the two most important problems, once these two problems are solved, it can be said that the transformation of the whole project is almost completed. What are the two questions?

  1. As a server-side frameworkeggHow do withwebpackIn combination with?
  2. useejsAs a template engine, how does it workdevThe environment andprodEnvironment correct willejsTo renderhtmlAnd display it on the page?

egg + webpack

Before we start working on the Egg layer, we need to check out the official documentation for the framework. As it is a product of Ali, the stability of the framework itself, the degree of ecological construction and the friendliness of the document must be guaranteed.

Egg is an “enterprise application framework” developed based on KOA. Simply understood, it encapsulates the request, response and all related operation methods on KOA, making it easier for ordinary developers to use, and focusing more on 996. It’s business development. It’s what you call hand in hand.

Egg follows the principle of convention over configuration, which is immediately apparent when compared to Express. Express is similar to jquery in terms of constraints. Routers are everywhere the heart wants them. What are middleware, Services, and Controllers?

This is not the case with Egg, which sacrifices freedom in favor of a more uniform writing style: business code goes to Controller, middleware goes to Middleware, SQL goes to Service, and the rest of the plugins and configurations have a uniform entry point, otherwise it wouldn’t work. In addition to the powerful plugin ecosystem support, flexibility is not weak.

Egg-webpack as a webpack plugin supported by egg ecology, you can directly NPM install a plug-in. When you shuttle, this thing is “devDependencies”, don’t put it in “devDependencies”.

Open the plug-in

/config/plugin.js /config/plugin.js /plugin.js /config/plugin.js /config/plugin.js

/ * *@type Egg.EggPlugin */
module.exports = {
  webpack: { // In the development environment, start the egg-webpack plugin
    enable: process.env.NODE_ENV === 'development'.package: 'egg-webpack',},ejs: {
    enable: true.package: 'egg-view-ejs',},static: {
    enable: true.package: 'egg-static',}};Copy the code

As you can see from the other two egg-view-ejs and egg-static plugins, one is a template engine plugin for EJS and the other is a static resource plugin.

The WebPack configuration file required to configure the plug-in

After installing and starting the plug-in in the previous step, you need to tell egg-Webpack where to find the native WebPack configuration file.

Open /config/config.local.js and write the following code:

/* eslint valid-jsdoc: "off" */

'use strict';

const path = require('path');

/ * * *@param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo= > {
  /**
   * built-in config
   * @type {{}} * * /
  const config = exports = {};

  // add your middleware config here
  config.middleware = [];

  // Enable webPack compilation in the development environment
  config.webpack = {
    // port: 9000, // port: {Number}, default 9000. The default port is 9000
    webpackConfigList: [ require('.. /build/webpack.dev.config')]};// In the development environment, change the directory for forwarding static resources from /app/public to/SRC /static (specific forwarding address can be defined by yourself).
  config.static = {
    prefix: '/public/'.dir: path.join(appInfo.baseDir, 'src/static'),};// add your user config here
  const userConfig = {
    // myAppName: 'egg',
  };

  return{... config, ... userConfig, }; };Copy the code

Note: Egg-webpack only needs to be turned on in the development environment, and the build script can be configured directly in package.json in the production environment

"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.config.js",
Copy the code

An egg will automatically according to the package. The json script command to find the right configuration files, for example, development mode will find/config/config. Default. Js and/config/config. Local. Js file merging; Production environment will find/config/config. The default. Js and/config/config. Prod. Js file to merge.

As for the Webpack configuration information in /build, it was detailed in the previous article and will not be covered here.

The config.static configuration marks requests prefixed with /public/ as static resource requests and forwards all requests to/SRC /static.

Ejs template fetching and rendering

In fact, how to take the EJS template in the development environment and synthesize the data to render it into HTML that the browser can recognize and then return, is the real soul step.

Enable EJS configuration

This is explained in the /config/plugin.js section of egg-webpack.

Configure the EJS view engine

Open/config/config. Default. Js write the following code:

/* eslint valid-jsdoc: "off" */

'use strict';

const path = require('path');

/ * * *@param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo= > {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}* * /
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1599807210902_4670';

  // add your middleware config here
  config.middleware = [];

  config.view = {
    mapping: {
      '.ejs': 'ejs',},defaultViewEngine: 'ejs'};// add your user config here
  const userConfig = {
    // myAppName: 'egg',
  };

  return{... config, ... userConfig, }; };Copy the code

Config. view is used to configure the template file suffix and the default template engine.

As the development environment and production environment needs the code snippet, so to write/config/config. Default. Js configuration file.

Egg server code

With egg-view-EJS configuration enabled, we’ll start coding the egg business.

Configure the /app/router.js file first:

'use strict';

/ * * *@param {Egg.Application} app - egg application
 */
module.exports = app= > {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  router.get('/welcome', controller.welcome.index);
};

Copy the code

Now, the/and /welcome requests are forwarded by the Egg to the corresponding Controller layer for processing. The controller is assembled and processed by the data request, and returns a rendered HTML to the page. Let’s look at what the controller does.

Since the project is a template framework, the back-end code does not involve database or middleware, so middleware and Services are not required, but they are almost essential if you want to start a secondary project from there.

Since the original front-end code is not stored directly in /app, all es6, style and template files are eventually compiled by Webpack into static resources, so /app/public and /app/ View should be empty in their initial state. Similar to the following figure

The controller file /controller/home.js is used as an example.

const Controller = require('egg').Controller;
const { render } = require('.. /utils/utils.js');

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    await render(ctx, 'home.ejs', { title: 'home' });
    // ctx.render('home.ejs', {title: 'home '});}}module.exports = HomeController;

Copy the code

As you can see, the code itself is very simple. The focus of the page rendering is on the render method. Let’s take a look at the /app/utils/utils.js file

The magic of render


const axios = require('axios');
// const ejs = require('ejs');
const CONFIG = require('.. /.. /build/config.dev');
const isDev = process.env.NODE_ENV === 'development';

function getTemplateString(filename) {
  return new Promise((resolve, reject) = > {
    axios.get(`http://localhost:${CONFIG.PORT}${CONFIG.PATH.PUBLIC_PATH}${CONFIG.DIR.VIEW}/${filename}`).then(res= > {
      resolve(res.data);
    }).catch(reject);
  });
}

/** * render method *@param CTX egg CTX object *@param Filename specifies the filename to render@param Additional objects * needed for data EJS rendering@return {Promise<*|undefined>}* /
async function render(ctx, filename, data = {}) {
  // File suffix
  const ext = '.ejs';
  filename = filename.indexOf(ext) > -1 ? filename.split(ext)[0] : filename;
  try {
    if (isDev) {
      const template = await getTemplateString(`${filename}${ext}`);
      ctx.body = await ctx.renderString(template, data);
    } else {
      await ctx.render(`${filename}${ext}`, data); }}catch (e) {
    return Promise.reject(e); }}module.exports = {
  getTemplateString,
  render,
};

Copy the code

You can see the internal implementation logic of the Render function:

  1. In a production environment, it’s very simple, just take the data and use iteggTo provide thectx.renderRender the specified template file
  2. In the case of a development environment, you need to request itself firsthttp://localhost:7001/public/view/*.ejsAccess to theejsThe source file string is then usedeggTo provide thectx.renderStringRender it to the page.

As for how to obtain the template, I have seen many teachers’ methods, one of which is to call webpack API to directly pull out the underlying memory files, and then manually call JS to compile the operation like a tiger, and finally render it out, turtle ~ anyway, I have seen a long time did not learn, And look at the amount of code feel that the workload is not cheap and to the compilation principle of Webpack research is quite deep, can have some achievements. You can explore that if you’re interested.

** Note: ** here is a very interesting point. If you think about it carefully, you will find that it is not true: Ejs /public/view/*. Ejs/SRC /view/*. Ejs /public/view/*. How does this map back?

Apart from an egg – static configuration static resource mapping, webpack has a layer of resource mapping, of her own. And I’m here webpack output. PublicPath written is just/public /, that is to say, When webpack compiles and generates files into memory, the memory access address is prefixed with /public/; The /public/view/*.ejs file accesses the resource file in webpack memory.

Personally, the pattern for accessing static resources in a development environment is to go to a different address in the egg-static and Webpack configurations and return whichever one is found.

Of course, static resource access in production is not a problem because webpack is not directly involved. You can use egg-static configuration for the same project, or you can use Nginx for static resource forwarding configuration across projects.

Hidden detail Easter eggs

At this point, the basic project is almost complete. There are still some details that need to be noted, and I’ll write them here to remind you and remind yourself:

Image Resource Path

If we write static resources such as images to ejS, there are two ways:

  1. /public/The prefix is the absolute path approach, and the thing to notice about this approach is:egglocalThe configuration file/config/config.local.jsIn the changedegg-staticStatic resource pointing to/src/static/. So the image resources are normally accessible in the Dev environment. But because of the production environmentegg-staticStatic resource pointing is the default/app/publicAnd the absolute path image reference form will not be usedwebpackIdentification processing, so be sure to ensure that the production environment/app/publicThe image resource is in the folder, otherwise it is a 404 resource request. If you use this image reference method, it is recommendedcopyWebpackPlug-ins that copy static resources in the production environment.
  2. written../The relative path form, the request form of the relative path, can be recognized and copied by WebPack normally, so there is no need for the developer to do extra processing. It’s just, becauseejsIs made up ofincludesFunctional, sometimes we might introduce some public onesejsCode blocks, and these code blocks are likely to have images and other reference resources. And be careful at this point, because this isincludesThe document, finallyincludesThe file will be spliced into the main file and then dropped tohtml-loaderParse, so the picture path of this piece needs to write the relative path under the main file, otherwise the picture can not be found.

As shown in the image above, the images produced by the two methods are obviously different, with the top one not packaged by Webpack and the bottom one obviously processed by WebPack.

Hot update

As for the hot update, this version is not quite the same as the last one, so there are some changes that need to be made:

First is/build/webpack. Base. Config. Js files:

module.exports = {
	// ...
    
  entry: (filepathList= > {
    const entry = {};
    filepathList.forEach(filepath= > {
      const list = filepath.split(/(\/|\/\/|\\|\\\\)/g);
      const key = list[list.length - 1].replace(/\.js/g.' ');
      // If it is a development environment, you need to introduce the Hot Module
      entry[key] = isDev ?
        [
          filepath,
          // The default port number for dev-server is 9000
          `webpack-hot-middleware/client? Path = http://127.0.0.1:9000/__webpack_hmr&noInfo=false&reload=true&quiet=false `,
        ] : filepath;
    });
    return entry;
  })(glob.sync(resolve(__dirname, '.. /src/js/*.js'))),
    
    // ...
};
Copy the code

In addition to Filepath, add Webpack-hot-middleware to the entry and concatenate configurations in queryString. The most important configuration is path=http://127.0.0.1:9000/__webpack_hmr, which specifies the address of the hot update websocket. Since the egg itself starts a different service than webpack-dev-server, if not configured, the default hot update will request port 7001, which is the development port, which will definitely not get anything.

/config/config.local.js /config/config.local.js /config/config.local.js /config/config.local.js /config/config.local.js /config/config.local.js /config/config.local.js /config/config.local.js /config/config.local.js

// Enable webPack compilation in the development environment
  config.webpack = {
    // port: 9000, // port: {Number}, default 9000. The default port is 9000
    webpackConfigList: [ require('.. /build/webpack.dev.config')]};Copy the code

If you do not want to use the default 9000, you can also change the default port, but also remember to change the webPack hot update configuration of the default port.

Finally, the Webpack-hot-Module native does not support hot updates to template files, as explained in the previous article. Therefore, the js entry file of each front-end page needs to add:

if (process.env.NODE_ENV === 'development') {
  // In a development environment, import ejS template files using raw-loader to force WebPack to treat them as part of the bundle that needs to be hot updated
  require('raw-loader! . /view/home.ejs');
  if (module.hot) {
    module.hot.accept();
    /** * listen for hot Module completion event, get template from server again, replace document * If you bind events to elements before, they may be invalidated after hot updates * 2. If the event is not destroyed before the module is unloaded, it may cause a memory leak. * The solution to both of these problems is to manually unbind the event before the Document.body content is replaced. * /
    module.hot.dispose(() = > {
      const href = window.location.href;
      axios.get(href).then(res= > {
        document.body.innerHTML = res.data;
      }).catch(e= > {
        console.error(e); }); }); }}Copy the code

Note: the above is a hot update code cannot be used to introduce into function, useless, I tried, can only in the entrance to each page file ctrlCV, of course, if you feel trouble, can not do this, is at best a template file changes won’t be hot update, not bother oneself refresh, effect.

conclusion

I remember my first job at my first company was to rewrite the website. The official website is packaged with a gulp compilation script written earlier, and gulp’s support for advanced ES6+ syntax is pathetic; To make matters worse, pure front-end code has no Node layer support and relies on Ajax to fetch data. Back then, back when the separation of the front and back ends wasn’t fully implemented, back when AngularJS dirty checks were going crazy, you had to open Eclipse to write your front-end code and wait for your back-end sibling to start working on it.

I started thinking that one day, developing multi-page apps on the front end would be as easy as shitting.

That would be nice.


The complete project address can be viewed on my Github complete project address, if you like to give a Star⭐️, thank you ~ your like, will be my continuous output of power 😃❤️


Previous content recommended

  1. 【 practical 】 WebPack4 + EJS + Express takes you through a multi-page application project architecture
  2. [Basic] Principles and applications of HTTP and TCP/IP protocols
  3. Thoroughly understand throttling and anti-shaking
  4. Event Loop in browser
  5. Interviewer: Talk about scopes and closures
  6. Interviewer: Tell me about the execution context
  7. Interviewer: Talk about prototype chains and inheritance
  8. Interviewer: Talk about modularity in JS
  9. Interviewer: Talk about let and const