Meta

Abstract

This article takes a click on the vue page element to jump to the corresponding vscode loader and plugin development practice, tells a simple introduction to webpack loader and plugin development.

The audience gains

Through this article, you can have a clearer understanding of webpack loader and plugin, and how to develop a loader and plugin, but also some vUE, CSS, Node related knowledge, expand your knowledge.

The effect

First effect:

Source warehouse

  • Github.com/zh-lx/vnode…
  • Github.com/zh-lx/vnode…

Front knowledge

Since we are developing loader and plugin, we need to have a simple understanding of the functions and components of loader and plugin.

loader

role

Loader is a tool that WebPack uses to convert different types of files into modules that WebPack can recognize. As we all know, Webpack only supports JS and JSON file processing by default. By using loader, we can convert files of other formats to JS format and let WebPack process them. In addition, we can also use loader to process and process the content of the file.

Constitute a

Loader essentially exports a JavaScript function that webpack calls through loader Runner and then passes in the results or resource files generated by the previous loader.

Example:

Loader / / synchronization

/ * * *@param {string|Buffer} Content Indicates the content of the source file *@param {object} (map) can be used by https://github.com/mozilla/source-map SourceMap data *@param {any} [meta] Meta data, can be any content */

module.exports = function (content, map, meta) {

  return someSyncOperation(content);

};
// or 

module.exports = function (content, map, meta) {

  this.callback(null, someSyncOperation(content), map, meta);

  return; // Always return undefined when callback() is called

};

// --------------------------------------------------------------------------

Asynchronous loader / /

module.exports = function (content, map, meta) {

  var callback = this.async();

  someAsyncOperation(content, function (err, result) {

    if (err) return callback(err);

    callback(null, result, map, meta);

  });

};

// or 

module.exports = function (content, map, meta) {

  var callback = this.async();

  someAsyncOperation(content, function (err, result, sourceMaps, meta) {

    if (err) return callback(err);

    callback(null, result, sourceMaps, meta);

  });
};
Copy the code

Refer to the API

Webpack.docschina.org/api/loaders…

plugin

role

Expand webpack to provide all functions that loader cannot complete.

Constitute a

A plugin consists of the following parts:

  • Export a named JavaScript function or JavaScript class.
  • Define one on the prototype of the plug-in functionapplyMethods.
  • Specifies an event hook bound to WebPack itself.
  • Handles specific data for webPack internal instances.
  • The callback provided by WebPack is invoked when the functionality is complete.
// A JavaScript class class MyExampleWebpackPlugin {// Define a 'apply' method on the prototype of the plug-in function, taking compiler as an argument. Apply (Compiler) {// specifies an event hook to mount to the Webpack itself. Compiler. Hooks. Emit. TapAsync (' MyExampleWebpackPlugin '(compilation, the callback) = > {the console. The log (' this is an example of a plugin! '); Console. log(' here represents the 'compilation' object for a single build of the resource: ', compilation); // Compilation. AddModule (/*... * /); callback(); }); } } module.exports = MyExampleWebpackPlugin;Copy the code

The compiler and compliation

There are two important concepts in the development of webpack Plugin: Compiler and Compliation.

There is a apply method in the Plugin class, which receives compiler as parameter. Compiler has been created at the beginning of Webpack construction and runs through the whole life cycle of Webpack. It contains all the options passed by webPack configuration files. For example, loader and plugins.

Compilation is when a module is ready to be compiled, the compilation object is created. It contains module resources, compile-build resources, and state information for changing files and tracked dependencies for the plug-in to work with. If we need to complete a custom compilation process in our plug-in, we will definitely use this object.

Refer to the API

Webpack.docschina.org/api/plugins…

The overall train of thought

  1. To click on an element to jump to vscode, you first need some means to open vscode and use a plugin to do the following:
  • Open vscode: the launchEditor method wrapped in react recognizes and wakes up various editors by starting vscode through node’s child_process API
  • Click on the element to notify a jump: start a Node Server service locally, click on the element to send a request, and then the Node Server triggers the jump
  1. In order to jump to the corresponding line and column of vscode, you need to know the source location of the clicked element, so you need a loader to inject the relevant information of the source into the dom during compilation

The implementation process

Realize the vnode – loader

debugging

Debug with loader-runner

In the process of loader development, we often need to break points or print some information for debugging. However, if we start Webpack every time, there may be many problems such as slow startup speed, too many project files and filtering information. Here we can use the loader Runner mentioned earlier for easy debugging.

Loader-runner exports a method called runLoaders, which is actually used to run various Loaders within Webpack. It receives four arguments:

  • Resource: The absolute path of the resource to be resolved
  • Loaders: An array of absolute paths for the Loaders to use
  • Context: Context attached to the Loader
  • ReadResource: A function that reads resources

Create a run-loader.js file in the root directory and fill in the following content. Run the node./run-loader command to run the loader and debug the breakpoint in the loader source code:

const { runLoaders } = require('loader-runner'); const fs = require('fs'); const path = require('path'); runLoaders( { resource: path.resolve(__dirname, './src/App.vue'), loaders: [path.resolve(__dirname, './node_modules/vnode-loader')], context: { minimize: true, }, readResource: fs.readFile.bind(fs), }, (err, res) => { if (err) { console.log(err); return; } console.log(res); });Copy the code
Debug in vue-CLI

Since we are using in vUE project, we debug loader through webpack configuration of VUE-CLI in order to match the real environment of VUE.

Json file. Add the following content to the.vscode/launch.json file, specifying that on port 5858, run the NPM run debug command to start a Node service:

{// Use IntelliSense to learn about related attributes. // Hover to view descriptions of existing properties. / / for more information, please visit: https://go.microsoft.com/fwlink/?linkid=830387 "version" : "0.2.0," "configurations: [{" type" : "node", "request": "launch", "name": "debug", "skipFiles": ["<node_internals>/**"], "runtimeExecutable": "npm", "runtimeArgs": ["run", "debug"], "port": 5858 } ] }Copy the code

Add the following command to the package.json file:

{"name": "loader-test", "version": "0.1.0", "private": true, "scripts": {"serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "debug": "node --inspect-brk=5858 ./node_modules/@vue/cli-service/bin/vue-cli-service.js serve" }, // ... }Copy the code

Click vscode debug to debug:

Parsing the template

To inject source information into the DOM, we first need to get the DOM structure of the.vue file. Then we need to parse the template part. We can use @vue/compiler-sfc to parse.vue files.

import { parse } from '@vue/compiler-sfc'; import { LoaderContext } from 'webpack'; import { getInjectContent } from './inject-ast'; /** * @description Inject line, column and path to VNode when webpack compiling. Vue file * @type webpack.loader */ function TrackCodeLoader(this: LoaderContext<any>, content: string) { const filePath = this.resourcePath; Let params = new URLSearchParams(this.resource); let params = new URLSearchParams(this.resource); if (params.get('type') === 'template') { const vueParserContent = parse(content); / / the vue file after the parse const domAst = vueParserContent. Descriptor. The template. The ast; // template starts dom ast const templateSource = domast.loc.source; // Const newTemplateSource = getInjectContent(domAst, templateSource, filePath); Const newContent = Content.replace (templateSource, newTemplateSource); // Template partial string const newContent = Content.replace (templateSource, newTemplateSource); return newContent; } else { return content; } } export = TrackCodeLoader;Copy the code

We analyze the above part of the code, first we export a TrackCodeLoader function, which is the vnode-loader entry function, through this object, we can get a lot of webpack and source code related information.

Params.get (‘type’) === ‘template’. For a.vue file, vue-loader breaks it up into parts and gives them to the parser it implements. For example, if there is a file path /system/project/app.vue, vue-loader will parse it into three parts:

  • /system/project/app.vue? type=template&xxx: This is the HTML part that will be built into VUEvue-template-es2015-compilerParse to dom
  • /system/project/app.vue? type=script&lang=js&xxxThis part, as the JS part, will be handed over to the matching WebPack configuration/.js$/Rule ofbabel-loaderWait for Loadre to handle it
  • /system/project/app.vue? type=style&lang=css&xxx: This part as CSS will be handed over to matching webPack configuration/.css$/Rule ofcss-loader,style-loaderTo deal with, such as

So a vue file will actually pass through our custom loader multiple times, and we only need to process the one time the type parameter in its URL is template, because only this time the template part of the code will be effectively processed into the DOM.

We then pass the contents of the.vue file as an argument to the parse function exported from @vue/ Compiler-SFC, and we get an object with a descriptor property in it, which we can see by making a breakpoint, Template, script, CSS, etc.

Now that we have the AST of the template structure, all we need to do is replace the domast.loc. source part of the.vue file’s content with the Template string that has been injected with the source information.

The AST of template is a tree structure that represents the current DOM node. The main properties that are relevant to the source information we inject are the following:

  • Type: indicates the current node type. 1 indicates the label node, 2 indicates the text node, and 6 indicates the label attribute…… Here we only need to inject the tag node, that is, we only need to inject thetype === 1Ast node for processing.
  • Loc: information about the current node in vscode, including the source information of the node in vscode, starting and ending rows, columns, and lengths in vscode. This is the information we want to inject
  • Childern: Performs recursive processing on child nodes

Injecting source information

We create a getInjectContent method to inject the source information into the DOM. The getInjectContent method takes three parameters:

  • Ast: indicates the AST of the current node
  • Source: The source string corresponding to the current node
  • FilePath: indicates the absolute path of the current file

Inject rows, columns, tag names, and file paths into dom tags:

Export function getInjectContent(ast: ElementNode, source: string, filePath: string) {if (ast? .type === 1) {if (ast.children && ast.children.length) {// Start with the last child, For (let I = ast.children. Length - 1; i >= 0; i--) { const node = ast.children[i] as ElementNode; source = getInjectContent(node, source, filePath); } } const codeLines = source.split('\n'); Const line = ast.loc.start.line; // Const line = ast.loc.start.line; // Start row of the current node const column = ast.loc.start.column; // Start column of the current node const columnToInject = column + ast.tag.length; // Column to inject information (one space after tag name) const targetLine = codeLines[line-1]; // Line to inject information const nodeName = ast.tag; const newLine = targetLine.slice(0, columnToInject) + ` ${InjectLineName}="${line}" ${InjectColumnName}="${column}" ${InjectPathName}="${filePath}" ${InjectNodeName}="${nodeName}"` + targetLine.slice(columnToInject); codeLines[line - 1] = newLine; // replace the injected content source = codelines.join ('\n'); } return source; }Copy the code

Realize the vnode – the plugin

Node Server wakes up VScode

We create a local Node service using http.createServer, and then use the protFinder package to find an available interface to start the service from port 4000. Node’s local service receives file, line, and column, and when it receives the request, wakes up vscode from launchEditor and opens the corresponding code location.

It is important to note that webPack regenerates a Compliation object every time it is compiled and plugin is run once, so we need a started flag to record whether any services have been started.

LaunchEditor is a REACT_EDITOR (VUE_EDITOR) file that can be used with.env.local. It is essentially the child_process module provided by Node. It recognizes the editor integration in operation and opens it automatically. By receiving file, line, and column parameters, it can open the specific file location and position the cursor to the corresponding row and column.

This part of the code is as follows:

// start the local interface and call vscode import HTTP from 'HTTP '; import portFinder from 'portfinder'; import launchEditor from './launch-editor'; let started = false; export = function StartServer(callback: Function) { if (started) { return; } started = true; Const server = http.createserver ((req, res) => {// Vscode const Params = new URLSearchParams(req.url.slice(1)); const file = params.get('file'); const line = Number(params.get('line')); const column = Number(params.get('column')); res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': 'Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE,X-URL-PATH,x-access-token', }); res.end('ok'); launchEditor(file, line, column); }); Portfinder.getport ({port: 4000}, (err: Error, port: number) => {if (err) {throw err; portfinder.getPort ({port: 4000}, (err: Error, port: number) => {if (err) {throw err; } server.listen(port, () => { callback(port); }); }); };Copy the code

Switch of control function

We need to be able to control the opening and closing of vscode by clicking on elements. There are many ways to realize the control switch, such as button combination trigger, suspension window… The control mode of suspension window is adopted here.

In the page to add a fixed location of the suspension window, I in the plug-in implementation of the suspension window can be dragged and moved, drag and style part of the code is not the focus, because it is not here to expand in detail, interested students can see the source code. The DOM part of the hover window is as follows: (This part of the code will be automatically injected into THE HTML through vNode-plugin, no need to manually add) :

<div id="_vc-control-suspension" draggable="true">V</div>
Copy the code

We use an is_Tracking variable to indicate whether the feature is enabled, and when clicking on the hover window, toggle the value of is_Tracking to switch the feature on or off (as discussed later) :

// enable let is_Tracking = false; const suspension_control = document.getElementById( '_vc-control-suspension' ); suspension_control.addEventListener('click', function (e) { if (! has_control_be_moved) { clickControl(e); } else { has_control_be_moved = false; }}); Function clickControl(e) {let dom = e.tag as HTMLElement; if (dom.id === '_vc-control-suspension') { if (is_tracking) { is_tracking = false; dom.style.backgroundColor = 'gray'; } else { is_tracking = true; dom.style.backgroundColor = 'lightgreen'; }}}Copy the code

Dom information is displayed when the mouse is moved

We add a fixed location mask globally, and then a Mousemove listening event.

If is_Tracking is true when the mouse moves, it indicates that the function is enabled, and via E.path we can find the DOM bubbling array that the mouse is hovering over. Take the first DOM with the _vC-path property injected, and then show the mask layer on the DOM using the setCover method.

The setCover method is mainly used to locate the mask layer on the target DOM, and set the size of the mask layer as large as the target DOM, and display the label, absolute path and other information of the target DOM (similar to the effect of viewing dom during Chrome debugging).

This part of the code is as follows:

// Window. addEventListener('mousemove', function (e) {if (is_tracking) {const nodePath = (e as any).path; let targetNode; if (nodePath[0].id === '_vc-control-suspension') { resetCover(); } // Find the first element with _vc-path attribute for (let I = 0; i < nodePath.length; i++) { const node = nodePath[i]; if (node.hasAttribute && node.hasAttribute('__FILE__')) { targetNode = node; break; } } if (targetNode) { setCover(targetNode); }}}); // When the mouse cursor moves over the corresponding information component, Function setCover(targetNode) {const coverDom = document.querySelector('#__COVER__') as HTMLElement; const targetLocation = targetNode.getBoundingClientRect(); const browserHeight = document.documentElement.clientHeight; / / browser height const browserWidth = document. DocumentElement. ClientWidth; Coverdom.style. top = '${targetLocation.top}px'; coverDom.style.left = `${targetLocation.left}px`; coverDom.style.width = `${targetLocation.width}px`; coverDom.style.height = `${targetLocation.height}px`; const bottom = browserHeight - targetLocation.top - targetLocation.height; // Distance from bottom of browser viewport const right = browserWidth-targetLocation. left-targetLocation. width; // Distance from browser to right const file = targetNode.getAttribute('_vs-path'); const node = targetNode.getAttribute('_vc-node'); const coverInfoDom = document.querySelector('#__COVERINFO__') as HTMLElement; const classInfoVertical = targetLocation.top > bottom ? targetLocation.top < 100 ? '_vc-top-inner-info' : '_vc-top-info' : bottom < 100 ? '_vc-bottom-inner-info' : '_vc-bottom-info'; const classInfoHorizon = targetLocation.left >= right ? '_vc-left-info' : '_vc-right-info'; const classList = targetNode.classList; let classListSpans = ''; classList.forEach((item) => { classListSpans += ` <span class="_vc-node-class-name">.${item}</span>`; }); coverInfoDom.className = `_vc-cover-info ${classInfoHorizon} ${classInfoVertical}`; coverInfoDom.innerHTML = `<div><span class="_vc-node-name">${node}</span>${classListSpans}<div/><div>${file}</div>`; }Copy the code

Click on the mask layer to send the request

Add click events to the Window set to capture phase (if it’s bubbling phase, element bound click events will occur first, affecting our click). If is_tracking is true, the first target element injected with source information is found according to e.path, and the trackCode method is called to send a request to wake up vscode. Both the bubble propagation () and e.preventDefault() methods are used to prevent the element’s default click events.

TrackCode basically takes the source information injected into the target dom, parses it into parameters, and requests the node server service we started earlier, which opens vscode through the launchEditor.

This part of the code is as follows:

// Press the corresponding function button to click the page, Window.addeventlistener ('click', function (e) {if (is_tracking) {const nodePath = (e as any).path; let targetNode; // Find the first element with _vc-path attribute for (let I = 0; i < nodePath.length; i++) { const node = nodePath[i]; if (node.hasAttribute && node.hasAttribute('__FILE__')) { targetNode = node; break; }} if (targetNode) {// Stop bubble propagation (); // block the default event e.preventDefault(); // Wake up vscode trackCode(targetNode); } } }, true ); // Open vscode function trackCode(targetNode) {const file = targetNode.getAttribute('__FILE__'); const line = targetNode.getAttribute('__LINE__'); const column = targetNode.getAttribute('__COLUMN__'); const url = `http://localhost:__PORT__/? file=${file}&line=${line}&column=${column}`; const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.send(); }Copy the code

Inject code into HTML

Finally we want to in the above code as into HTML, HTML – webpack – the plugin provides a htmlWebpackPluginAfterHtmlProcessing hook, We can inject our code at the bottom of the body in this hook:

import startServer from './server'; import injectCode from './get-inject-code'; class TrackCodePlugin { apply(complier) { complier.hooks.compilation.tap('TrackCodePlugin', (compilation) => { startServer((port) => { const code = injectCode(port); compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap( 'HtmlWebpackPlugin', Data.html = data.html. Replace ('</body>', '${code}\n</body>'); }); }); }); } } export = TrackCodePlugin;Copy the code

Access to the process

You can experience it in your own VUE3 project by following the following process:

  1. The installationvnode-loaderandvnode-plugin:
yarn add vnode-loader vnode-plugin -D
Copy the code
  1. Modify thevue.config.js, add the following code (be sure to use only in development environments) :
/ /... other code module.exports = { // ... other code chainWebpack: (config) => { // ... other code if (process.env.NODE_ENV === 'development') { const VNodePlugin = require('vnode-plugin'); config.module .rule('vue') .test(/.vue$/) .use('vnode-loader') .loader('vnode-loader') .end(); config.plugin('vnode-plugin').use(new VNodePlugin()); }}};Copy the code
  1. Add a name named in the project root directory.env.localThe following is the content of:
# editor

VUE_EDITOR=code
Copy the code
  1. In vscodeCommand + Shift + P, the inputshell Command: Install 'code' command in PATHAnd click the command:

If the following popup window is displayed, success is achieved:

performance

Some people may worry that the plug-in will slow down the speed of webpack packaging and compilation. After several comparative tests of large projects, the speed of Webpack build and rebuild is almost the same before and after using the Loader and plugin, so it can be boldly connected.

conclusion

Now we should have a certain understanding of webpack loader and plugin development, with the help of custom loader and plugin really can do a lot of things beyond imagination (especially plugin, most of the time only need an imagination), You can use your imagination to write your own loader and plugin to help project development.

reference

Concept | webpack Chinese documents

Juejin. Cn/post / 690146…