Nuxt deep practice

Vue and React give us a good low-level MV* framework, on top of which we need to consider some further issues:

  1. How isomorphic (SSR, CSR)?
  2. How to improve the speed of business iteration?
  3. How to do front-end modularization and engineering better?
  4. How to reduce deployment and release costs?

Why use Nuxt

Many interviewers like to ask about the technology selection of the project. Why do you want to use such a framework to meet your requirements? If someone asks me this question, I will say something like:

  • Progressive is actually what we’re thinking about. As an Internet product, all kinds of daily activities and irregular operation activities are inevitable. A lot of repetitive logic can be managed in modules. Nuxt’s Module provides convenient “plug and play” module management. So far, we have about 10 large modules in our project, as well as several business plug-ins, which help us solve a lot of the code reuse issues.
  • Vue is based on the team stack. Of course, if your team’s main stack is React, you can also choose Nuxt’s cousin Nest.
  • Since our team’s Node services are all deployed based on K8S + Docker, Nuxt, which has almost no server-side language dependency, is of course the preferred choice. If your service is deployed more on physical machines, using Egg or PM2 + Koa/Express + Nuxt is also an option to take advantage of multi-core performance.
  • Static, Nuxt also has good support for generating Static pages, which is useful because many activities expire and still need to save a snapshot. Can greatly reduce the cost of our snapshot generation. For these reasons, we began to iterate on our Nuxt module in late 19, and now we have thousands of QPS traffic H5 plugged into Nuxt. Admittedly, we encountered many problems during business iteration. The Whiteboard discussion helped us better complete functional iteration based on our business scenarios and gradually understand these problemsProgressive frameworkDesign ideas.

What can be done with the Module

At the very beginning, we built a bare-bones project framework based on Express and Nuxt, just simple SSR. We started thinking about how to plug in existing programs.

The migration of the old

Our previous SSR projects were implemented based on a very old internal framework.

How old is it? No async, no ES6 syntax, code management based on SVN. Many node modules are C++ modules compiled through node-gyp, and the C++ source code has been lost. The Node version is always stuck in Node 0.12.x, even when we were docker on this project, we had to build our own Node image to serve as the base image for the business. Such a core technical debt, you need to pay step by step. We broke down all the functional blocks of the old code (most of which were obsolete) and found some that could be removed in advance for the new project. PlatformDetect, Auth, Toast, JSBridge, etc. At this point, how to refactor old code is a very technical work. As the saying goes, the pace is too long to pull x. To ensure the stability of existing projects and the ability to iterate over the new framework in the business, to ensure business progress and rapid migration iterations, you need to think before you start. Based on the above four old function modules, we took the necessity and portability into consideration and planned the first-phase scheme:

  1. General-purpose logic modules such as platformDetect, commonly known as utils, can be organized into a single module that provides the Server /client plugin for business (and our own, small team woe) calls, using old code first.
  2. Auth is a very basic and important module. Whether the whole page can be opened and displayed normally requires this module to verify. In addition, since our logon logic is relatively complex, it has been stomped and redesigned for many times and finally reached a release level.
  3. Toast, strictly speaking, is a UI module, but considering that this module can be injected through plug-ins, and basically all H5 uses this function, we also raise the priority of this module and rewrite it.
  4. JSBridge is also a very basic, but widely used module, but this module is introduced through the WAY of SDK, and only need to be called in the client side, the previous code can be directly used, after all, JavaScript can be backward compatible, So we’ll just pull the SDK out and inject it as a separate module that other projects can reference.

Create a new

For most projects, the old modules that need to be migrated are generally necessary and business-oriented modules. In addition, H5 business will be migrated from physical machine deployment to Docker deployment, and the overall service will be microservitized. We need to add many modules to provide some of the functionality needed in K8S.

monitoring

No matter what service architecture you use, monitoring in place can keep your business running in a stable, secure environment. Based on the K8S environment, Prometheus is a universal solution. Of course, other monitoring schemes can also be implemented with similar ideas. The monitoring we consider is based on several dimensions:

  • The response time
  • Upstream status
  • traffic
  • Business error response time is actually very simple. Based on the Middleware model of Express, we only need to implement a middleware, record a startTime when the request enters, and use the current time-start time at the end of the request to get the response time of the entire request. Start work!
// monitorMiddleware. Js // any monitor reported SDK const promClient = require(' prom-client '); const monitorReport = serviceName => {returnasync (req, res, next) => { const { path } = req; const startTime = Date.now(); await next(); try { const responseTime = Date.now() - startTime; promClient.report(serviceName, path, responseTime); } } } module.exports = monitorReport; // server.js const app = express(); App. Use (monitorMiddleware (" demo "));Copy the code

The run. Ideal is full, reality is dregs. We haven’t received any surveillance reports. By omiting numerous debugging procedures, we find that the first half of the middle part of the request executes normally after entering the Node service, but the logic after await next() does not execute. After searching the source code and documentation, IT turns out that Nuxt actually takes over all the business that goes into the Nuxt rendering process and Nuxt returns it. This code is how to call Nuxt when SSR:

// server.js
app.use((req: Express.Request, res: Express.Response) => {
  nuxt.render(req, res);
});
Copy the code

Nuxt will spit out its rendered results directly back to the client. Fortunately, Nuxt provides hooks that allow us to intervene in the Nuxt lifecycle. We used the render:routeDone life cycle to conduct SSR service monitoring. According to the official documentation, this life cycle will be in

Every time a route is server-rendered. Called after the response has been sent to the browser.

That is, after SSR rendering has finished and the response has been returned to the client. Response time reporting can be done on this hook (in fact, most reporting is done at this stage). We organized hook and Middleware into a module so that all businesses could inject dependencies with one click and plug into the entire monitoring system.

// module.js // This is a simplification of the module code, of course, in order to implement the module, we still need to structure the file directory rendererHook = require('./hooks/render');
module.exports = function prometheus(options) {
	const { serviceName } = options;
  this.addServerMiddleware({
    path: '/metrics',
    handler: metrics,
  });
  this.addServerMiddleware({
    path: '/'Handler: monitorMiddleware (' demo '),}); this.nuxt.hook('render:routeDone', rendererHook.routeDone); } // hooks/render.js const onRenderRouteDoneHook = (url, result, context) => { try { const { path } = context.req; const { statusCode } = context.res; const resSize = JSON.stringify(result).length; const renderTime = Date.now() - context.res.startTime; const isError = statusCode === 200 ? 0:1; // resSize: flow // renderTime: renderTime // isError: render error prom.report()}}Copy the code

The alarm

Since the company has an established Sentry platform, it would be nice to have resources that can be used directly, and the Nuxt community provides many great modules, including the NuxT-Sentry module. After taking the module down, I found that some areas needed to be modified to fit the company’s environment, but in most cases it was straightforward to use.

Data request

The same Nuxt community provides the NuxT-AXIos module to help us make data requests easier. However, we still need to make some changes to Axios to fit the environment, such as:

  • Requesting different interfaces for development and production is unavoidable if you also have test and online environments in the background and cross domain constraints.
  • Replay the request if the request times out or fails.
  • Sets the request expiration time and returns a custom error code after a timeout.
  • Uniformly inject some custom information for all requests, etc. Nuxt-axios provides a singleton instance of AXIOS to call. We can directly wrap nuxT-AXIos once without touching the internal logic. We just need to import both modules when we import them. Assuming we name the module nuxT-Request, we can get such a module entry file.
// request.js
module.exports = function request(moduleOptions) {
  const { timeout = 3000 } = moduleOptions;
  this.addPlugin({
    src: path.resolve(__dirname, './plugins/request.server.js'),
    fileName: 'request.server.js',
    mode: 'server',
    options: {
      timeout,
    },
  });
  this.addPlugin({
    src: path.resolve(__dirname, './plugins/request.client.js'),
    fileName: 'request.client.js',
    mode: 'client',
    options: {
      timeout,
    },
  });
}
Copy the code

We inject two different plugins, and Nuxt introduces different plugins at Runtime, depending on the environment in which the code is running. The options parameter is also interesting in Nuxt. Dynamically inject some variables into the plug-in (in the EJS template language) so that the business side can dynamically inject information into the plug-in when calling the module or plug-in in a way configured in nuxt.config.js. For example, we dynamically injected the axios request timeout into the plugin.

// request.server.js
export default function axios(context, inject) {
  const { $axios, req } = context; Const {userVid} = req.cookies. UserVid;$axios.onRequest(config => { const { url } = config; // Using the interceptor approach, we can do a lot of things, such as injecting request IPif (url.startsWith('http') || url.startsWith('https')) {
      config.baseURL = 'xxxx'; } // For example, inject user information config.params && config.params.userVid = userVid; Config. timeout = <%= options.timeout %>; }); }Copy the code

Nuxt.config.js provides us with a configuration approach, and the template Plugin function opened in the plug-in can implement many configuration based extensions. For example, the timeout Settings above. Also, since the context contains the context of each request, we can inject a single request for each different server request without manually passing the request context to upstreams each time we invoke it. Of course, thanks to Nuxt’s ability to inject plug-ins on both sides, the client side can intercept and process requests in a different way than the server side. Suppose we reject some requests from clients to prevent sensitive data interfaces from being exposed to the Internet. Here are the plugins used by the client:

// request.client.js
export default function axios(context, inject) {
  const { $axios, req } = context;
  const wrapper = fn => {
    if(Object.prototype.toString.call(fn) ! = ='[object Function]') {
      throw new Error('fn must be a function, but got: ', typeof fn);
    }
    return(url, ... args) => {if (url.startsWith('/user/secret')) {
        return Promise.reject('request forbidden');
      }
      returnfn(url, ... args); } } const get = wrapper($axios.get);
  const post = wrapper($axios.post);
  inject('axios', {
    get,
    post,
  });
}
Copy the code

We then import these two plug-ins separately in the module entry and configure them in nuxt.config.js. Nuxt provide modules, plug-in, middleware and hooks can make us on the basis of the framework, do plug and play of the expansion of business, when a function module is used by multiple business, you need to consider it out to make a module to release out, in the team in a wide range of business, this approach can greatly improve the efficiency of development. GitHub – lerna/ Lerna: A tool for managing JavaScript projects with multiple packages. To manage these Nuxt modules, LerNA’s multi-packages management and release log make module publishing easier and clearer, converging all Nuxt dependencies together.

Nuxt and K8S

Today’s front-end development environment has changed a lot from a few years ago. We initially deployed services directly on physical machines to provide services through our own Node.js framework, which caused a lot of problems.

  1. When abnormal traffic enters the service, there will be a surge in machine load, but there is no sophisticated way to limit the amount of node resources. If the machine resources are full (especially memory), many requests will be blocked, resulting in an avalanche effect.
  2. Horizontal expansion cost is high, and not flexible and elastic;
  3. The cost of building a service monitoring system is also very high, many things need to “build rockets”;
  4. Poorly architecting CI/CD processes can result in high deployment costs and inconvenient rollbacks.

Because of the above problems, as well as online services have encountered many problems. We migrated all the services to K8S and implemented business deployment based on Docker. Here is not a detailed description of a series of monitoring based on K8S, CI/CD and other processes, because it may need to understand the knowledge of K8S, if you are interested in the back can be opened separately between the front end and K8S. So what do we get from k8s?

  • Dynamic capacity expansion: When the service response time reaches a bottleneck, the service is automatically expanded.
  • Service quality Monitoring based on Prometheus;
  • A set of CI/CD process implemented by K8S simplifies the process and time from code submission to release online;
  • Ensure the granularity of services. Active services can be online when needed, and all services can be directly offline when not needed, which can better save hardware costs.

Some tricks used by Nuxt

There are some specific scenarios that need to be addressed when implementing a business with Nuxt, and there are some tricks that have been found that will be updated as we continue to iterate

Some hidden hooks

When compatible with the Kindle browser, we found that no matter how much we lowered the Target version of Babel, JavaScript would not execute properly, but the page rendering was fine, and after a series of debugs, we found the problem. The built-in browser version of The Kindle is very low, probably a decade or so ago, safari, this browser does not even support IIFE, and Babel compiled JavaScript, almost all contain IIFE, even the most outer layer of IIFE. So, we need to redirect the JavaScript script from the SSR page to another script, which is implemented with a lower version of native JavaScript, to ensure that we can bind event listeners and so on on the Kindle. So how to replace, in SSR, Nuxt will take over the service into its own route, rendering directly into HTML strings to spit back to the front end. The first thing that came to mind was hooks, but disappointingly we didn’t seem to find any hooks in the documentation that would get the render results. When browsing the source code, we found a built-in hook, this hook is not documented, guess it may be a hook used by Nuxt or vue hook.

// @nuxt/vue-renderer/dist/vue-renderer.js
    // Template params
    const templateParams = {
      HTML_ATTRS: meta ? meta.htmlAttrs.text(true /* addSrrAttribute */) : ' ',
      HEAD_ATTRS: meta ? meta.headAttrs.text() : ' ',
      BODY_ATTRS: meta ? meta.bodyAttrs.text() : ' ',
      HEAD,
      APP,
      ENV: this.options.env
    };

    // Call ssr:templateParams hook
    await this.serverContext.nuxt.callHook('vue-renderer:ssr:templateParams', templateParams);

    // Render with SSR template
    const html = this.renderTemplate(this.serverContext.resources.ssrTemplate, templateParams);

    let preloadFiles;
    if (this.options.render.http2.push) {
      preloadFiles = this.getPreloadFiles(renderContext);
    }

Copy the code

This hook is vue – the renderer: SSR: templateParams, this hook templateParams provide a parameter, this parameter provides several attributes, are SSR rendering results:

HTML_ATTRS: 'data-n-head-ssr',
HEAD_ATTRS: ' ',
BODY_ATTRS: ' ',
HEAD: '<title>nuxt-demo</title>',
APP: '<div data-server-rendered="true" id="__nuxt"><! ----><div id="__layout"><div><div class="demo"><div class="demo_center"></div></div></div><script src="/_nuxt/runtime.js" defer></script><script src="/_nuxt/commons.app.js" defer></script><script src="/_nuxt/vendors.app.js" defer></script><script src="/_nuxt/app.js" defer></script>'
ENV: { baseUrl: 'https://localhost:3000/demo'}}Copy the code

As you can see, JavaScript scripts are inserted into the APP property. We tried to replace the script in the APP property with our own script URL through regular expression, and found that the script pulled on the client side is our own low script. Of course, using an unexposed interface to solve the problem may not be the best solution, but until a better solution is found, this method is crude but can achieve our function (if there is a more reasonable implementation of the comments, please let the author know ~).

// server.js
async function start () {
  // Init Nuxt.js
  const nuxt = new Nuxt(config);
  const { host, port } = nuxt.options.server;

  nuxt.hook('vue-renderer:ssr:templateParams', (templateParams: TemplateParams) => { console.log(templateParams); const APP = templateParams.APP; const removedScriptApp = APP.replace(/<script\b((? ! utils|global|localStorageUtil|collectHtml|login|shelf|reader|>).) *><\/script>/g,' ');
    templateParams.APP = removedScriptApp;
    return templateParams;
  });

  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt);
    await builder.build();
  } else {
    await nuxt.ready();
  }

  // Give nuxt middleware to express
  app.use((req: Express.Request, res: Express.Response) => {
    nuxt.render(req, res);
  });

  // Listen the server
  app.listen(port, host);
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  });
}
start();
Copy the code

SSR also supports the Express API

In the process of business development, we found that many apis need to be used in both server-side rendering and client-side rendering. These apis are not provided by the background, but we need to assemble some data in the background by ourselves and make a series of serial or parallel requests for data assembly. Since Nuxt’s asyncData method is called to request when rendering on the server side, and the background API is directly requested when rendering on the client side, if we directly implement it through the middleware of Express, AsyncData then needs to request the local localhost to get the result of the request, which is not very reasonable because it will consume node service traffic and waste a TCP connection. There are several good implementations that we have explored:

  1. Separate the API from Nuxt, and these apis are aggregated in other services. This also has problems, a business development time, need to start two projects to do, API service is still not good serverless;
  2. Extract the transform of data splicing and write the data request separately inasyncDataAnd Express Middleware. The way we’re doing it now works well when the API is a parallel request, and the transform isomorphism is actually quite easy. However, in the process of serial requests, multiple transforms are still needed to handle this problem, which was encountered in subsequent business requirements, so we considered a third option.
  3. Separate the API into a single module: service. The Service is responsible for encapsulating API requests and data aggregation. Each API can be encapsulated as a function to differentiate between different environments and inject different Request objects. For each service module, they do not need to care about which environment they are in or which request object they use to get data, but through the unified injectionaxiosInstance to make the request.
// userService.js
module.exports = async functionuser (context, ... args) { const {$axios } = context.app;
  const { userVid } = context.req.query;
  const userData = await $axios.get('/user/profile? userVid=' + userVid);
  const { userName, gender } = userData;
  const friendData = await $axios.post('/user/friend? userVid=' + userVid + '&gender=' + gender);
  return{... userData, ... friendData, }; } // service.server.js is provided as a server plugin to asyncData module.exports =functionservice (context, inject) { const services = getAllService(); // Read all service const serviceMap = new Map(); services.forEach(service => { const fn = require(service); serviceMap.set(service, (... args) => { fn(context, ... args); }); }); inject('service', { request: (serviceName, ... args) => {returnserviceMap.get(serviceName)(... args); }}); } // service.js is provided as an SDK to serverMiddleware const services = getAllService(); Const axios = require(axios); // Const axios = require(axios); const serviceMap = new Map(); services.forEach(service => { const fn = require(service); serviceMap.set(service, (req) => {return(... args) => {return fn({
        app: { $axios: axios }, req: { ... req }, }, ... args); }; }); }); module.exports =function service (serviceName, req) => {
	return serviceMap.get(serviceName)(req);
}
Copy the code

Here to achieve desensitization, so write relatively simple, but the implementation of the idea has been more clear, the core is through the injection of function parameters, to smooth Nuxt and Express in the environment difference.

Write in the last

Nuxt provides a lot of ideas for our containerization SSR development, but also provides a lot of solutions for our front-end engineering, but Nuxt itself is relatively low-level, just for Vue SSR function has been expanded, leaving a lot of space for development. There are still a lot of questions to be answered in the course of our practice. Of course, the overall business architecture and business scenario of each company will be different, and this article will be updated with the business iteration (I just finished working with typescript recently, so I will continue to add them when I have the energy).