Webpack good solution

Webpack is a well-known front-end development tool, which can build a development environment including hot updates, can also generate compressed production environment code, but also has flexible scalability and rich ecological environment. However, its disadvantages are also very obvious, that is, the configuration items are many and complex, take any configuration items (such as rules, plugins, devtool, etc.) can be enough to write an article explaining its N uses, causing great confusion for beginners. Vue.js (Vue) is mostly built using Webpack, indirectly leaving the problem to Vue novices. However, both Vue and WebPack know what the configuration problem is, so they have come up with their own solutions to this problem. Let’s take a look at their efforts.

Vue CLI 2.x – Provides out-of-the-box configuration

A long time ago, we initialized a Vue project using the Vue init command provided by VUe-CLI (also Vue CLI version V2, later referred to as Vue CLI 2.x). And usually some of the larger projects will use vue init Webpack my-project to use the Webpack template, so the configuration problem mentioned above comes into play.

In order to solve this problem, Vue provides out-of-the-box configuration, which is generated by Vue init. By default, a large number of configuration files are generated.

It’s guaranteed out of the box, but if you want to change it, you’re in a black box, and developers can’t handle a bunch of files and JSON.

Webpack 4 – Greatly simplifies configuration

Webpack 4 has been around for about a year now, and one of its core changes is a huge simplification of configuration. It adds mode to make some of the obvious configurations built-in. So NoEmitOnErrorsPlugin(), UglifyJSPlugin() and so on don’t need to be written; The CommonsChunkPlugin() for subcontracting is also condensed into a configuration item, optimization.splitchunks, and has default values that will work for most cases.

Webpack 4 is also said to be building smaller code, so this upgrade is clearly necessary.

Vue CLI 3.x – Upgrade webpack, also out of the plug-in

About half a year ago, Vue CLI released version V3, which was also a disruptive upgrade. It reduces the core to @vue/ CLI, turns Webpack into @vue/ CLI-service, and abstracts everything else into “plug-ins.” These plug-ins include Babel, ESLint, Vuex, Unit Testing, and more, and allow for custom authoring and publishing. I won’t cover the usage and ecology of Vue CLI 3.x here, but as a result, Vue projects created through Vue Create are now much cleaner.

So what’s the problem now?

If we are simply developing a front-end Vue project, webpack-dev-server can help us start a NodeJS server and support hot loading, which is very useful. But what if we want to develop a full stack of NodeJS + Vue? It’s impossible to boot on the same port. All we can do is have NodeJS start on port A and Vue (Webpack-dev-server) start on port B. However, if Vue needs to send requests to access the API provided by NodeJS, it will encounter cross-domain problems, which can be solved by configuring proxy, but it is still very complicated. In essence, these are the back and forth ends of an entire project, and we should start them with one command, one port.

Aside from Vue, such requirements are actually supported by Webpack itself. Because it provides Webpack-dev-server as well as Webpack-dev-Middleware. It also integrates hot loading in express Middleware. Therefore, if our NodeJS uses Express as the service framework, we can introduce the middleware as app.use and achieve the fusion of the two.

Back to Vue CLI 3. It achieves brevity by wrapping webpack and webpack-dev-server with vue-cli-service so that the user can’t see the configuration file. But in essence, the configuration file is still there, just moved to node_modules / @ vue/cli – service/webpack config. Js. Of course, for personalized needs, it also allows users to configure objects (configureWebpack) or chainWebpack indirectly, but does not provide direct modification of the configuration file.

Fatally, though, it doesn’t turn Webpack-dev-server into Webpack-dev-middleware, even though it provides enough ways to modify the configuration. This means that the Vue part created using Vue CLI 3 and the NodeJS (Express) part cannot be fused.

How to solve it?

Having said so much, in fact, this is the problem I actually encountered recently and the way to analyze the problem. Due to the nature of the Vue CLI 3 black box, we can no longer use it (there may be a future update that will fix this, at least for now). Using Vue CLI 2 is also confusing because it comes with WebPack 3 and a lot of configuration files. At this point, the only way left is to use and configure WebPack 4 yourself, which is where this article comes in.

Technology stack

Nodejs part

One of the most popular Web frameworks for nodeJS is Express. The main reason for this is that webpack-dev-middleware is express middleware. So the two can work seamlessly together.

Unfortunately, in my actual project, I use KOA as my NodeJS framework. I can’t say how it’s better than Express, and that’s not the point of this article. Maybe it was for the first time, or for the unity of the team’s tech stack, or some other weird coincidence, but I used it and didn’t realize it was a fusion until I realized webpack-dev-middleware wasn’t compatible with KOA, which I regretted a little bit… That’s another story, of course.

This paper is based on KOA. If you’re using Express, it’s much the same, and much simpler.

Vue part

There’s not much to say about Vue, just one version, no express/KOA/other options. But instead of USING SSR, I used a normal SPA project (single page application, front-end rendering).

The directory structure

Since it is a combination of two projects, there is always a problem of directory structure arrangement. I’m not going to talk about how each project needs to be organized internally, that’s a Vue/KOA issue and a matter of personal preference. What I want to talk about is the organization of the two, which is nothing more than the following three :(in fact, it is a matter of personal preference, different opinions, here is just a unified expression, to avoid subsequent confusion)

The front and back end projects in the following screenshots are standalone projects, that is, before fusion, the kind that can run separately, so you can see two copies of package.json and package-lock.json

The back-end project is the base, and the front-end project is the subdirectory

Except for the vue directory in the red box, everything else is nodeJS code. And since I’m just making a gesture, the nodejs code actually only contains two index.js, a public directory, and two package.json. The actual NodeJS project would have more code, such as Actions (which handles each route in a separate directory), Middlewares (middleware that goes through all routes), and so on.

The idea is that the front end is part of the project (page presentation), so the Vue is placed in a separate directory. And that’s the structure THAT I used.

The front-end project is the base, and the back-end project is the subdirectory

This is the opposite of the previous one, where the red box is the back-end code. The reason for this arrangement is probably because we are front-end developers, so we put the front-end code at the base, and the API provided by the back end assists the Vue code to run.

Neutral, not favoring anyone

Looking at the first two, it is natural to think of this third method. I don’t think this is a problem, because NPM requires package.json to be in the root directory, so it actually does more harm than good to separate the two completely and treat them fairly (i.e., multiple layers of various call paths).

Modify the Vue part

The transformation points of Vue are as follows:

  1. Package. json is merged into package.json in the root directory (nodejs). This includes dependency (Dependency and devDependency) and execution commands (scripts). The rest of the fields, browserslist, Engine, etc. that Babel might use, can be copied directly because nodeJS code doesn’t need Babel, and there is no merging.

  2. Write a webpack. Config. Js. (Since Vue CLI 3 is automatically generated and hidden, you need to write this yourself)

Let’s look at it in detail.

Fusion package. Json

As mentioned earlier, Babel fields such as Browserslist and Engine are not needed on the NodeJS side, so simply copy them. It’s dependence and command that takes the brain.

In terms of dependencies, there are almost no dependencies shared by the front and back ends, so it’s actually a simple copy. Note that vue, VUe-Router, webpack, webpack-CLI, etc are devDependency, not dependency. The only dependency I really need is @babel/ Runtime (plugin-transform-Runtime).

On the command side, the Vue itself must have “start the development environment” and “build” two commands (as well as test, which I won’t discuss here). Since the development environment needs to be integrated with NodeJS, we’ll put this in the NodeJS section. What remains is the build command, and the general action is to get webpack to go online by setting NODE_ENV to production. Also note that since package.json and webpack.config.js are no longer sibling directories, you need to specify additional directories as follows :(cross-env is a pretty handy tool for setting environment variables across platforms)

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config vue/webpack.config.js"}}Copy the code

Write a webpack. Config. Js

The focus of this article is not on how WebPack is configured, so I won’t go into the details of what each configuration item means

Webpack.config.js is essentially a configuration file that returns JSON, and we’ll use a few of those keys. If you want to know all the configuration items of Webpack, you can see the Introduction of Webpack in Chinese. Also, if you don’t want to view it in sections, you can find the full webpack.config.js here.

mode

Webpack 4 adds configuration items with general optional values ‘production’ and ‘development’. Here we determine the value according to process.env.node_env.

let isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development'
}
Copy the code

entry

Define the entry to webPack. We need to set the entry to the JS that created the Vue instance, such as Vue/SRC /main.js.

{
  entry: {
    "app": [path.resolve(__dirname, './src/main.js')]}}Copy the code

output

Define the output configuration for Webpack. In development mode, Webpack-dev-Middleware (WDM) doesn’t actually generate the dist directory; it outputs files to memory through an in-memory file system. So the directory is just an identifier.

{
  output: {
    filename: '[name].[hash:8].js'.path: isProd ? resolvePath('.. /vue-dist') : resolvePath('dist'),
    publicPath: '/'}}Copy the code

resolve

Two main things are defined: the order of suffixes automatically added when Webpack processes imports and aliases for quick access.

{
  resolve: {
    extensions: ['.js'.'.vue'.'.json'].alias: {
      'vue$': 'vue/dist/vue.esm.js'.The '@': resolvePath('src'),}}}Copy the code

Module (Emphasis)

Modules in WebPack mainly determine how to handle the different types of modules in a project. We use the most common configuration here, which tells Webpack which suffix file to use which loader.

{
  module: {
    rules: [{test: /\.vue$/.loader: 'vue-loader'
      },
      {
        test: /\.js? $/.loader: 'babel-loader'.exclude: file= > (
          /node_modules/.test(file) && !/\.vue\.js/.test(file)
        )
      },
      {
        test: /\.less$/.use: [
          isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader'.'css-loader'.'less-loader'] {},test: /\.css$/.use: [
          isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader'.'css-loader'}]}}Copy the code

There are four file processing modes configured above, which are:

  1. /\.vue$/

    Process Vue files using the VUe-loader that Vue provides specifically. What this handler does is separate the

    It also requires a special plug-in, VueLoaderPlugin(), as you’ll see later, don’t miss it.

  2. /\.js? $/

    This is ostensibly used to process files with the.js suffix, but is essentially used here to process the contents of

  3. /\.less$/

    I use less as a style preprocessor in my project, so I use

    1. vue-style-loader

      Insert styles as

      It’s not that different from style-loader, but since the official Vue documentation recommends this, let’s go with it.

    2. The mini – CSS – extract – plugin loader

      Split the styles into a separate CSS file and refer to them as in the tag, replacing the extract-text-webpack-plugin in webpack 3.x for use only in production.

      It also needs to add the MiniCssExtractPlugin() to the plug-in to work with it.

    3. css-loader

      Implicit loading of resources is supported. For example, if you write import ‘style.css’ in a JS file, or a URL (‘image.png’) in a style file, CSS and image.png can be added to the webpack process by csS-loader. This allows all loaders for CSS to handle style.css, and all image loaders (such as urL-loaders that automatically convert to Base64 for small sizes) to handle image.png.

    4. less-loader

      Loader that must be loaded using less preprocessors to convert less syntax into normal CSS syntax.

      Basically each preprocessor has a corresponding loader, such as Stylus-Loader, Sas-Loader, etc., which can be used on demand.

  4. /\.css$/

    Similar to the.js rule, this rule applies both to.css postfixes and to the

Plugins

Plug-ins, like rules, handle resources loaded into webpack. However, unlike rules, it does not use the re (mostly suffixed names) to decide whether to enter or not. Instead, it uses the plugin’s own JS notation to determine what to do with all entries, so it is more flexible.

As mentioned earlier, some functions require loader and plugins to work together and therefore need to be declared here, such as VueLoaderPlugin() and MiniCssExtractPlugin().

In our project, plug-ins fall into two categories. One category is used regardless of the environment (development or production), there are two categories:

{
  "plugins": [
    // Work with vue-loader
    new VueLoader(),

    // Output index. HTML to output
    new HtmlwebpackPlugin({
      template: resolvePath('index.html')]}})Copy the code

The other type is the production environment only need to use, there are two:

if (isProd) {
  webpackConfig.plugins.push(
    // Each build clears the output directory
    new CleanWebpackPlugin(resolvePath('.. /vue-dist'))
  )
  webpackConfig.plugins.push(
    / / separate CSS file to the output, and MiniCssExtractPlugin loader
    new MiniCssExtractPlugin({
      filename: 'style.css',}}))Copy the code

optimization

Optimization is a new configuration item in WebPack 4 that deals with all kinds of optimizations in production (such as compression, extraction of common code, etc.), so most optimizations are used when mode === ‘production’ is used. Here we use only one of its function, namely the subcontract, before writing is the new webpack.optimize.Com monChunkPlugin (), now just configuration, configuration method is simple:

{
  optimization: {
    splitChunks: {
      chunks: 'all'}}}Copy the code

Thus, the code from node_modules is bundled together, named Vendors ~app.[hash].js, and it might contain Vue, Vuex, Vue Router, and so on. This code doesn’t change very often, so isolating it and adding a long mandatory cache can significantly speed up site access.

Look at the finished product

By running NPM run build, you can call Webpack-CLI to run the configuration file you just wrote. Vue = () => import(‘@/ xxx.vue ‘); vue = () => import(‘@/ xxx.vue ‘);

There are four files

  1. Index. HTML holds a unique HTML entry that contains references to various JS and CSS files and defines container nodes. After starting with a static server, front-end rendering can be performed due to JS execution.

  2. Style.css stores all styles. These styles are pulled from the

  3. App. [hash].js holds all custom js, i.e. the

  4. Vendors ~app.[hash].js, as mentioned above, hosts all the js class libraries, such as VUe-Router, and the code for vuex itself.

As far as Nodejs is concerned, the index. HTML is all it cares about. It imports the other three. So let’s look at how to transform the NodeJS section.

Modify the NodeJS (KOA) section

The main points we need to improve in KOA are as follows:

  1. package.json

    Nodejs project standard configuration, record dependencies, scripts, project information and so on. We need to merge here with package.json on the Vue side, especially with the NPM Run Dev script.

  2. index.js

    Nodejs code entry to start the entire project with the command node index.js. You can register routing rules here, register middleware, register WDM, and so on. Most of the logic is here.

    Because the development and production environments behave differently (for example, the development environment requires WDM and the production environment does not), it can be divided into two files (index.dev.js and index.prod.js) or judged by environment variables in one file, which varies from person to person.

    Although koA routing rules and middleware can be written here, it is common for small projects to store routing and middleware in separate actions and Middlewares directories (depending on your preference). Configuration files (such as config port numbers) are also usually separate into config.js or config directories. Other directories such as util are also built on demand.

    We need to unify front and back routing here and use WDM etc

package.json

As described in package.json in the “Modify Vue section”, Vue project dependencies are copied directly to the outer package.json, and an NPM run build command is added. Here are two more commands to meet the most basic requirements.

I split index.js into index.dev.js and index.prod.js in order to differentiate the runtime environment. As mentioned above, you can also use process.env.node_env in a single file to determine the runtime environment.

Start the development server

For a regular KOA service, we usually start it with Node index.js. However, nodeJS is not hot loaded by default, so modifying nodeJS code requires a restart of the server, which is quite troublesome. I used to use the Chokidar module to listen for changes to the file system and execute delete require.cache[path] when changes were made to implement a simple hot loading mechanism. But now there’s a more convenient tool that does that for us, and that’s Nodemon.

"nodemon -e js --ignore vue/ index.dev.js"
Copy the code

It is also very simple to use. Replace Node index.js with Nodemon index.js, and it will listen for all file changes executed through this portal and restart automatically. But we also use two additional configuration items here. -e specifies the extension. In this case, we only listen for JS. — Ignore specifies the item to be ignored because webpack is in the vue/ directory to do the hot loading for us, so its changes can be ignored. For other available configuration items, see the home page of Nodemon.

Start the online environment server

This is easy, just execute the node command. So the final part of the script looks like this:

{
  "scripts": {
    "dev": "nodemon -e js --ignore vue/ index.dev.js"."build": "cross-env NODE_ENV=production webpack --config vue/webpack.config.js"."start": "node index.prod.js"}}Copy the code

index.js

This file is the startup entry for KOA, and it looks like this (I used the KOA-Router to manage the routes, and just listed the bare bones) :

// Reference the base library
const Koa = require('koa')
const Router = require('koa-router')
const koaStatic = require('koa-static')

/ / initialization
const app = new Koa()
const router = new Router()

// A regular project might have middleware, which is the logic that handles all the routing, such as verifying logins, logging, etc., omitted here

// Register a route to the koa-router.
// There are a lot of regular project routes, should be independent to a directory to register one by one
router.get('/api/hello', ctx => {
  ctx.body = {message: 'Greeting from koa'}})// The KOA-router is registered with the KOA as middleware
// this is the way to write it
app.use(router.routes());
app.use(router.allowedMethods());
// Start static services for the public directory, which can hold images, fonts, etc. Vue partially packaged resources are in VUe-dist, not here.
app.use(koaStatic('public'));

// The actual project may also write the port to the configuration file
app.listen(8080)
Copy the code

We discuss how to modify this file from both the development environment and the online environment.

The development environment

The first is to merge the front – and back-end routes.

The Vue side uses the History routing mode, so nodeJS is needed to work with it. Vue officially recommends connect-History-API-Fallback middleware, but that’s for Express. I found a middleware with the same functionality for KOA called KOA2-history-API-Fallback.

Regardless of the middleware, the principle is the same. Since the SPA generates only one index. HTML, all navigate routes must be directed to this file otherwise, for example, /user/index, the browser would look for /user/index.html and would not find it.

Since Vue requires all NAVIGATE routes, it clearly cannot register before koA’s routes, otherwise koA’s routes will never take effect. Therefore, the route registration order is natural: first end, then front end. The navigate route refers to the first HTML request, not the static resource request, so the order between the public static route and the Vue middleware, for example, is irrelevant.

So the route fusion looks something like this:

// Back-end (KOA) routing
// The single registration part of the koa-router is omitted
app.use(router.routes());
app.use(router.allowedMethods());

// Front-end (VUE) routing
// All navigate requests are redirected to '/' as webpack-dev-middleware only serves this route
app.use(history({
  htmlAcceptHeaders: ['text/html'].index: '/'
}));
app.use(koaStatic('public'));
Copy the code

The second is the use of Webpack-dev-middleware, which started the problem.

Like Vue’s middleware, Webpack-dev-Middleware is Express-only (all of which suggests that Express has a better ecosystem), and I found a koA version of the alternative called KoA-WebPack.

It’s not too much trouble to use, as follows:

const koaWebpack = require('koa-webpack')
const webpackConfig = require('./vue/webpack.config.js')

// Note that this is asynchronous, so other app.use and eventually app.listen must be executed together
// This can be guaranteed with async/await or Promise
koaWebpack({
  config: webpackConfig,
  devMiddleware: {
    stats: 'minimal'
  }
}).then(middleware= > {
  app.use(middleware)
})
Copy the code

A full index.dev.js can be viewed here.

The online environment

There are two differences between an online environment and a development environment, and we’ll focus on them.

First, the online environment doesn’t use Webpack-dev-Middleware (koa-webpack), so this part of the code is not needed.

Second, because the built Vue code is all in the VUe-dist directory, where the HTML entry and other JS and CSS files we need are, we need to add the vue-dist directory to the static service for access. In addition, the goals of the History Fallback have changed as follows:

// Back-end (KOA) routing
// The single registration part of the koa-router is omitted
app.use(router.routes());
app.use(router.allowedMethods());

// Front-end (VUE) routing
// All navigate requests are redirected to the /vue-dist/index.html file, in line with koaStatic('vue-dist') below, where simply enter '/index.html'.
app.use(history({
  htmlAcceptHeaders: ['text/html'].index: '/index.html'
}));
app.use(koaStatic('vue-dist'));
app.use(koaStatic('public'));
Copy the code

A full index.prod.js can be viewed here.

So many configuration novice looked or very ignorant how to do?

Despite all the discussion, don’t be afraid, there are really only three key points that we can summarize:

  1. We need to write our own Vue webpack.config.js, handle loaders, plugins, etc

  2. We need to merge the two package.json on the front and back end, merge the two dependencies, and write three scripts (dev, build, start).

  3. We need to change index.js, handle the routing order, and call webpack-dev-middleware in the development environment

To make it easy to get started, I took the business code out of the project and left a skeleton that could be used as a startup template for the Vue + KOA project in easonyq/vue-nodejs-template. However, I think it’s important to understand the configuration methodology and principles so that if a piece of the stack changes (e.g., WebPack 5), we can work on it ourselves, rather than trying to solve the task first and then running it.

May we all be able to go more smoothly on the front-end road!